Skip to content

feat: Add comprehensive PnL tracking with Relay API integration#466

Merged
RanaBug merged 23 commits intostagingfrom
feature-pnl
Dec 4, 2025
Merged

feat: Add comprehensive PnL tracking with Relay API integration#466
RanaBug merged 23 commits intostagingfrom
feature-pnl

Conversation

@aldin4u
Copy link
Collaborator

@aldin4u aldin4u commented Nov 25, 2025

  • Implement PnL calculation using Relay API for accurate buy/sell tracking
  • Add PnLStats component with 4-column layout (Balance USD, Bought, Sold, Floating PnL, Total PnL)
  • Add TokenPnLCell component for portfolio list PnL display
  • Create useTokenPnL hook for fetching and calculating PnL metrics
  • Add auto-refresh (30s) and manual refresh support for PnL data
  • Integrate PnL display in Buy and Sell components
  • Add skeleton loading animations for PnL components
  • Update My Holdings layout with improved column spacing and sort indicators
  • Add Relay API service for fetching transaction data
  • Add wallet transactions API integration
  • Implement PnL utilities with comprehensive test coverage

Description

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Summary by CodeRabbit

  • New Features

    • Four-column P&L summary widget, per-token unrealised PnL cells, and a "Hide <$0.5" toggle in token list; sortable headers for Price, Balance, and PnL.
  • Improvements

    • PnL wired to wallet transactions with periodic auto-refresh and on-demand refetch; improved loading UX with row and P&L skeletons; consistent color/formatting across Buy, Sell, and portfolio.
  • Tests

    • Expanded mocks and coverage for PnL, wallet transactions, search, and portfolio flows.
  • Chores

    • UI dependency bumped.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 25, 2025

Walkthrough

Adds end-to-end PnL: types and utilities for trade reconstruction, a polling hook (useTokenPnL) that fetches wallet transactions and relay data, RTK Query slices and async helpers, new PnL UI components and skeletons, wiring into Search/Buy/Sell, and test/mocks updates.

Changes

Cohort / File(s) Summary
PnL UI & Skeletons
src/apps/pulse/components/PnLStats/PnLStats.tsx, src/apps/pulse/components/PnLStats/PnLStatsSkeleton.tsx, src/apps/pulse/components/PnLStats/StatGroup.tsx
New PnLStats component plus StatGroup and PnLStatsSkeleton: four-column layout, formatting helpers, signed/percent PnL display, color coding, and loading skeleton.
Per-token UI & Search
src/apps/pulse/components/Search/TokenPnLCell.tsx, src/apps/pulse/components/Search/SkeletonTokenRow.tsx, src/apps/pulse/components/Search/SortIndicator.tsx, src/apps/pulse/components/Search/PortfolioTokenList.tsx, src/apps/pulse/components/Search/Search.tsx
TokenPnLCell (per-token PnL hook wiring and display), skeleton rows and sort indicator; PortfolioTokenList gains hideSmallBalances and isFetching props, filtering/sort scaffolding; Search adds "Hide <$0.5" toggle and forwards state.
PnL Hook
src/hooks/useTokenPnL.ts
New hook useTokenPnL (UseTokenPnLProps/TokenPnLResult): input validation, polling/refetch (30s), relay fetches, trade reconstruction, metric calculation, status/debug lifecycle, refetch API, unmount safety, and minimal loading duration.
RTK Query APIs & Async
src/services/relayApi.ts, src/services/pillarXApiWalletTransactions.ts, src/services/relayApiAsync.ts, src/store.ts
Added relay and wallet-transactions RTK Query slices with transformResponse, async store-based helpers for non-React contexts, and middleware registration in store.
PnL Utilities & Types
src/utils/pnl.ts, src/types/api.ts
New utilities: reconstructTrades, calculatePnL, getRelayValidatedTrades, calculatePnLFromRelay; new types: MobulaTransactionRow, WalletTransactionsMobulaResponse, ReconstructedTrade, PnLMetrics.
Buy / Sell Integration
src/apps/pulse/components/Buy/Buy.tsx, src/apps/pulse/components/Sell/Sell.tsx, src/apps/pulse/components/Buy/BuyButton.tsx, src/apps/pulse/components/App/HomeScreen.tsx
Wired wallet transactions and useTokenPnL into Buy/Sell flows, render PnLStats conditionally, refetch hooks; BuyButton accepts additional BuyOffer shape; HomeScreen forwards isRefreshing.
Tests & Mocks
src/utils/__tests__/pnl.test.ts, src/apps/pulse/components/*/tests/*, src/apps/key-wallet/*/__tests__/*
New unit tests for PnL logic; many tests updated to mock useTokenPnL, RTK Query hooks, add ThemeProvider wrappers and TestWrapper for deterministic runs.
Search helpers & constants
src/apps/pulse/components/Search/*, src/apps/pulse/constants/tokens.ts
New small UI components (skeleton, sort indicator); exported allStableCurrencies.
Misc / Formatting / Config
multiple src/apps/key-wallet/*, src/apps/pulse/*, package.json
Numerous formatting/import reorders, minor non-functional edits, and @mui/joy version bump.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI (Buy / Sell / Search)
    participant Hook as useTokenPnL
    participant RTK as RTK Query (walletTxs)
    participant Relay as relayApi
    participant Utils as PnL Utils
    participant Display as PnLStats / TokenPnLCell

    UI->>Hook: request PnL (token, wallet, chain, balance, price)
    Hook->>RTK: query wallet transactions
    RTK-->>Hook: transactions (MobulaTransactionRow[])
    Hook->>Utils: group & reconstruct trades
    loop per txHash
      Utils->>Relay: fetch relay request by hash
      Relay-->>Utils: RelayRequest | null
    end
    Utils->>Utils: calculatePnLFromRelay / calculatePnL
    Utils-->>Hook: ReconstructedTrade[] / PnLMetrics
    Hook-->>UI: { metrics, isLoading, refetch }
    UI->>Display: render PnLStats / TokenPnLCell (metrics or skeleton)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60-75 minutes

  • Focus areas:
    • src/utils/pnl.ts — trade reconstruction, decimal/USDC normalization, edge-case guards.
    • src/hooks/useTokenPnL.ts — async orchestration, polling, lifecycle/status handling, isMounted guard.
    • RTK endpoints & transforms: src/services/relayApi.ts, src/services/pillarXApiWalletTransactions.ts, src/services/relayApiAsync.ts.
    • Integration surfaces: TokenPnLCell / PortfolioTokenList / Buy / Sell wiring and combined loading/refetch logic.
    • Tests & mocks — ensure consistent RTK hook mocking and provider wrappers.

Possibly related PRs

Suggested reviewers

  • vignesha22
  • IAmKio

Poem

🐇 I hopped through ledgers, stitched relay crumbs to charts,
Rebuilt each trade with careful paws and tiny arts.
Green carrots, red beets — numbers twitched into view,
PnL now hums in rows, neat, signed, and true.
🥕 — a rabbit’s tally, lightly hopped for you.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lists key features and components added but lacks required sections: testing environment details, test results, screenshots, and completed types-of-change selections are all missing or incomplete. Complete the template by documenting testing environment and procedures, adding test results or evidence, providing relevant screenshots, and selecting applicable change types in the checkboxes.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main objective: adding comprehensive PnL tracking with Relay API integration, which is the primary feature of this changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature-pnl

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

❤️ Share

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

@aldin4u aldin4u requested a review from vignesha22 November 25, 2025 21:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (6)
src/store.ts (2)

35-47: addReducer path resolution is reasonable, but consider tightening types

Using reducerPath || name with a runtime console.error guard works, but it still allows misconfigured slices to slip through until runtime. You could type addReducer to accept only known slice/API shapes (e.g. Slice | Api union) so that either name or reducerPath is guaranteed at compile time, avoiding the need for a runtime error path.


73-79: init placeholder reducer keeps configureStore happy but hurts RootState typing

Seeding the store with { init: (state = {}) => state } is a pragmatic way to avoid an empty reducer map, but it also means RootState is inferred with an init key and no knowledge of dynamically added slices. If you rely on RootState for typed selectors, consider instead:

  • Seeding middlewareReducers.init before configureStore, and
  • Passing middlewareReducers directly into configureStore so that the inferred state better matches the dynamic shape.
src/services/relayApi.ts (1)

79-155: Unify base URL usage and consider future pagination support

RELAY_API_URL is used for fetchRelayRequests and fetchRelayRequestByHash, but fetchRelayRequestsByUser hardcodes the URL string. For consistency and easier maintenance, it’s worth switching that call to \${RELAY_API_URL}/requests/v2`and, if you later need full history, plumbingnextCursorfromRelayRequestsResponse` into a simple pagination loop.

Since this relies on the current Relay API shape, please verify that /requests and /requests/v2 still expose requests and (for v1) nextCursor as expected, and that there isn’t a more idiomatic “by user” endpoint you should be targeting.

src/hooks/useTokenPnL.ts (1)

167-177: Auto-refresh interval depends on props object identity and may restart on every render

The auto-refresh effect lists [props] as its sole dependency. If callers invoke the hook with an inline object (e.g. useTokenPnL({ token, transactionsData, walletAddress, chainId })), props will be a new reference on each render, so this effect will clear and recreate the interval every render, effectively resetting the 30s timer.

To make the interval more stable, depend on primitives instead, for example:

- useEffect(() => {
-   if (!props) return;
+ useEffect(() => {
+   if (!props) return;
    const interval = setInterval(() => {
      setResult(prev => ({ ...prev, isLoading: true }));
      setRefreshTrigger(prev => prev + 1);
    }, 30000);
    return () => clearInterval(interval);
- }, [props]);
+ }, [!!props]);

or use walletAddress / props.token.contract as dependencies if you only want to reset the timer when the target token/wallet truly changes.

Please verify how TokenPnLCell constructs the props object when calling this hook so you can choose the tightest, stable dependency set.

src/apps/pulse/components/Search/PortfolioTokenList.tsx (1)

102-147: Separate filtering from sorting to avoid duplicated work and hidden deps

getFilteredPortfolioTokens both filters and sorts using sortConfig, but portfolioTokens is created via useMemo that does not list sortConfig as a dependency, and you then sort again in sortedTokens.

To simplify and make the behavior clearer:

  • Keep getFilteredPortfolioTokens purely for filtering (including hideSmallBalances and search).
  • Move all sorting logic into the sortedTokens useMemo (which already depends on sortConfig).
    This avoids double-sorting and removes an implicit dependency on sortConfig from a memoized function that doesn’t declare it.
src/utils/pnl.ts (1)

19-45: Normalizing timestamp units across trade reconstruction paths

reconstructTrades assigns timestamp = tx.timestamp directly, whereas getRelayValidatedTrades uses timestamp / 1000 and calculatePnLFromRelay uses new Date(req.createdAt).getTime() / 1000. While PnL math doesn’t depend on absolute units today (timestamps are only used for sorting), this inconsistency can become problematic if consumers later display dates or merge trades from multiple sources.

Consider normalizing all trade timestamps to seconds since epoch (or milliseconds, but consistently) across all helpers so downstream consumers can rely on a single time unit.

Please double‑check Mobula’s timestamp unit (seconds vs milliseconds) to pick the correct normalization factor when you standardize these helpers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 654bd55 and 098d813.

📒 Files selected for processing (12)
  • index.html (3 hunks)
  • src/apps/pulse/components/PnLStats/PnLStats.tsx (1 hunks)
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx (8 hunks)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx (1 hunks)
  • src/apps/token-atlas/components/TokenGraphColumn/TokenGraphColumn.tsx (3 hunks)
  • src/hooks/useTokenPnL.ts (1 hunks)
  • src/services/pillarXApiWalletTransactions.ts (1 hunks)
  • src/services/relayApi.ts (1 hunks)
  • src/store.ts (4 hunks)
  • src/types/api.ts (2 hunks)
  • src/utils/pnl.test.ts (1 hunks)
  • src/utils/pnl.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • index.html
  • src/services/pillarXApiWalletTransactions.ts
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/store.ts
📚 Learning: 2025-10-15T10:31:06.760Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 432
File: src/containers/Main.tsx:135-141
Timestamp: 2025-10-15T10:31:06.760Z
Learning: In src/containers/Main.tsx, the React Native webview messaging setup intentionally only activates when the `devicePlatform` URL parameter is present ('ios' or 'android'). On reloads or direct navigation without URL params, the site should behave normally and not attempt to re-establish RN messaging. This is by design to ensure the site functions properly in both RN webview and standard browser contexts.

Applied to files:

  • index.html
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/apps/pulse/components/PnLStats/PnLStats.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/hooks/useTokenPnL.ts
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/token-atlas/components/TokenGraphColumn/TokenGraphColumn.tsx
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required text field. The TileTitleProps interface aligns with the TokensMarketData.title type in api.ts which also has optional fields.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/types/api.ts
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required title field. The TileTitleProps structure aligns with how it's used in the TokensMarketData type in api.ts.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/types/api.ts
🧬 Code graph analysis (8)
src/services/pillarXApiWalletTransactions.ts (2)
src/utils/blockchain.ts (1)
  • isTestnet (33-39)
src/types/api.ts (1)
  • WalletTransactionsMobulaResponse (792-801)
src/utils/pnl.test.ts (2)
src/types/api.ts (1)
  • MobulaTransactionRow (768-790)
src/utils/pnl.ts (2)
  • reconstructTrades (12-106)
  • calculatePnL (108-184)
src/apps/pulse/components/Search/TokenPnLCell.tsx (4)
src/services/tokensData.ts (1)
  • Token (19-29)
src/types/api.ts (1)
  • WalletTransactionsMobulaResponse (792-801)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (26-180)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/hooks/useTokenPnL.ts (4)
src/services/tokensData.ts (1)
  • Token (19-29)
src/types/api.ts (2)
  • WalletTransactionsMobulaResponse (792-801)
  • PnLMetrics (815-826)
src/utils/pnl.ts (2)
  • calculatePnL (108-184)
  • calculatePnLFromRelay (362-499)
src/services/relayApi.ts (2)
  • RelayRequest (3-77)
  • fetchRelayRequestByHash (107-130)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
src/types/api.ts (1)
  • PnLMetrics (815-826)
src/apps/pulse/components/Search/PortfolioTokenList.tsx (3)
src/types/api.ts (1)
  • PortfolioData (739-748)
src/services/tokensData.ts (2)
  • Token (19-29)
  • chainNameToChainIdTokensData (234-255)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/utils/pnl.ts (2)
src/types/api.ts (3)
  • MobulaTransactionRow (768-790)
  • ReconstructedTrade (803-813)
  • PnLMetrics (815-826)
src/services/relayApi.ts (2)
  • RelayRequest (3-77)
  • fetchRelayRequestByHash (107-130)
src/store.ts (1)
src/services/pillarXApiWalletTransactions.ts (2)
  • pillarXApiWalletTransactions (21-48)
  • pillarXApiWalletTransactions (50-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (7)
index.html (1)

4-61: Confirm env placeholder replacement and TradingView script path

The GA/Plausible tags and Hypelab script all depend on %VITE_GOOGLE_ANALYTICS_MEASUREMENT_ID% / %VITE_PLAUSIBLE_DOMAIN% being replaced at build time, and on /charting_library/charting_library/charting_library.js being available at that path in all deploy environments. Please confirm these are wired into your HTML build pipeline (or static hosting config) so you don’t ship literal %VITE_*% strings or a broken TradingView import.

src/types/api.ts (1)

184-188: PnL-related API types look consistent; verify against Mobula schema

MobulaTransactionRow, WalletTransactionsMobulaResponse, ReconstructedTrade, and PnLMetrics line up with how they’re consumed in useTokenPnL and src/utils/pnl.ts (fields like tx_hash, asset.contract(s), amount_usd, and the PnL metric names are all present and typed as number). The Projection.data union reformat is also fine.

Please double‑check these shapes against the latest Mobula wallet-transactions API response (especially data.transactions[...] vs result.data[...], and required vs optional asset.contract / contracts) so you don’t get runtime shape mismatches.

Also applies to: 768-826

src/store.ts (1)

94-99: RTK Query wallet-transactions API is wired correctly into dynamic middleware

Registering pillarXApiWalletTransactions via addMiddleware aligns with how the other API slices are integrated (reducer + middleware via createApi), so the new slice will participate in caching/polling as expected.

Please double‑check against the current RTK Query docs that pillarXApiWalletTransactions’s reducerPath and middleware are configured as intended in your version of @reduxjs/toolkit.

src/services/pillarXApiWalletTransactions.ts (1)

1-50: Wallet-transactions RTK Query slice looks consistent with the Mobula response type

The fetchBaseQueryWithRetry setup, getWalletTransactions query body, and transformResponse unwrapping into WalletTransactionsMobulaResponse are coherent, and the slice should compose cleanly with the dynamic addMiddleware wiring in src/store.ts.

Please confirm against the Mobula/HiFi data API docs that the path: 'wallet/transactions' payload and { result: WalletTransactionsMobulaResponse } envelope match the live response shape for both testnet and mainnet URLs.

src/hooks/useTokenPnL.ts (2)

93-101: I'll help you verify the hash field inconsistency issue. Let me search the codebase to confirm whether tx.hash and tx.tx_hash are indeed being used inconsistently.
<function_calls>


#!/bin/bash

First, let's find and examine the pnl.ts file to see how getRelayValidatedTrades uses tx fields

fd -t f 'pnl.ts' --exec cat -n {} ;


</function_calls>

Now let me search for the specific usage of tx_hash and tx.hash in the codebase:
<function_calls>


#!/bin/bash

Search for tx.tx_hash usage

echo "=== Searching for tx.tx_hash usage ==="
rg 'tx.tx_hash' -n -B2 -A2

echo ""
echo "=== Searching for tx.hash usage in context ==="
rg 'tx.hash' -n -B2 -A2 --type=ts


</function_calls>

Let me also check the type definitions to understand what fields are available on transaction objects:
<function_calls>


#!/bin/bash

Search for type definitions related to transactions

echo "=== Looking for Mobula transaction type definitions ==="
rg -n 'interface.[Tt]ransaction|type.[Tt]ransaction' --type=ts | head -20

echo ""
echo "=== Checking useTokenPnL.ts for context ==="
cat -n src/hooks/useTokenPnL.ts | sed -n '85,110p'


</function_calls>

<function_calls>


#!/bin/bash

Let's check the getRelayValidatedTrades function specifically

echo "=== Searching for getRelayValidatedTrades function ==="
rg -n 'getRelayValidatedTrades' -A 15 --type=ts


</function_calls>

<function_calls>


#!/bin/bash

Let's also check if there's any documentation or comments about Relay API

echo "=== Checking for Relay API references ==="
rg -n 'Relay|requests/v2' -B2 -A2 --type=ts | head -40


</function_calls>

<function_calls>


#!/bin/bash

Check if there are any type imports or definitions for transaction data from Mobula

echo "=== Looking for Mobula type imports ==="
rg -n 'from.mobula|import.[Mm]obula' -A3 --type=ts

echo ""
echo "=== Searching for transactionsData structure ==="
rg -n 'transactionsData.data.transactions' -B5 -A5 --type=ts


</function_calls>


108-123: I'll verify the concerns raised in this review comment about sequential Relay API calls. Let me start by examining the implementation and checking for relevant documentation.
<function_calls>

find . -name "useTokenPnL.ts" -type f

</function_calls>
<function_calls>

wc -l src/hooks/useTokenPnL.ts

</function_calls>
<function_calls>

sed -n '90,140p' src/hooks/useTokenPnL.ts

</function_calls>
<function_calls>

rg -n "fetchRelayRequestByHash" -A 3 -B 3

</function_calls>
<function_calls>

rg -n "RelayRequest" --type ts -C 2 | head -50

</function_calls>

Let me also check for Relay API documentation and rate limit information:
<function_calls>

Relay API rate limits documentation

</function_calls>
<function_calls>

Relay protocol fetchRelayRequestByHash rate limit concurrency

</function_calls>

Now let me check if there are existing caching mechanisms in the codebase:
<function_calls>

rg -n "cache|Cache|memoiz" --type ts src/hooks/ | head -30

</function_calls>
<function_calls>

rg -n "Promise.all" --type ts | head -20

</function_calls>

Let me examine the broader context of the useTokenPnL.ts file:
<function_calls>

sed -n '1,50p' src/hooks/useTokenPnL.ts

</function_calls>
<function_calls>

rg -n "import.*fetchRelayRequestByHash" src/hooks/useTokenPnL.ts -A 5

</function_calls>

Let me check where fetchRelayRequestByHash is defined:
<function_calls>

rg -rn "export.*fetchRelayRequestByHash|function fetchRelayRequestByHash" --type ts

</function_calls>

<function_calls>

find . -name "*.ts" -type f -exec grep -l "fetchRelayRequestByHash" {} ;

</function_calls>

src/utils/pnl.ts (1)

191-353: I'll help you verify this review comment. Let me examine the relevant code to confirm the issues identified.
<function_calls>

cat -n src/utils/pnl.ts | sed -n '191,353p'

</function_calls>

Let me search for both functions and their USDC_ADDRESSES definitions:
<function_calls>

rg -n "USDC_ADDRESSES" src/utils/pnl.ts -A 10 -B 2

</function_calls>

<function_calls>

rg -n "calculatePnLFromRelay" src/utils/pnl.ts -A 100 | head -150

</function_calls>

<function_calls>

rg -n "inTxs|outTxs" src/utils/pnl.ts -B 2 -A 2

</function_calls>

Now let me check the full structure of both functions to understand the context better:
<function_calls>

wc -l src/utils/pnl.ts

</function_calls>

<function_calls>

cat -n src/utils/pnl.ts | sed -n '1,100p'

</function_calls>

<function_calls>

rg -n "export const (getRelayValidatedTrades|calculatePnLFromRelay)" src/utils/pnl.ts

</function_calls>

<function_calls>

cat -n src/utils/pnl.ts | sed -n '191,370p'

</function_calls>

<function_calls>

grep -n "calculatePnLFromRelay" src/utils/pnl.ts | head -5

</function_calls>

<function_calls>

cat src/utils/pnl.ts | tail -300 | head -150

</function_calls>

Let me get the exact line numbers for calculatePnLFromRelay:
<function_calls>

ast-grep --pattern 'export const calculatePnLFromRelay'

</function_calls>

<function_calls>

python3 << 'EOF'
with open('src/utils/pnl.ts', 'r') as f:
lines = f.readlines()
for i, line in enumerate(lines, 1):
if 'calculatePnLFromRelay' in line or 'getRelayValidatedTrades' in line:
print(f"{i}: {line.rstrip()}")
EOF


</function_calls>

<function_calls>

tail -400 src/utils/pnl.ts | head -200

</function_calls>

<function_calls>

python3 << 'EOF'
import re

with open('src/utils/pnl.ts', 'r') as f:
content = f.read()

Find all USDC_ADDRESSES definitions

usdc_matches = list(re.finditer(r'USDC_ADDRESSES\s*=\s*[(.*?)]', content, re.DOTALL))
print(f"Found {len(usdc_matches)} USDC_ADDRESSES definitions\n")

for i, match in enumerate(usdc_matches):
start = content.rfind('\n', 0, match.start()) + 1
end = content.find('\n', match.end())
line_num = content[:match.start()].count('\n') + 1
context_start = max(0, match.start() - 200)
context_end = min(len(content), match.end() + 100)

print(f"Match {i+1} at line ~{line_num}:")
print(content[context_start:context_end])
print("\n" + "="*80 + "\n")

Find inTxs and outTxs usage

print("\ninTxs and outTxs usage:")
for match in re.finditer(r'(inTxs|outTxs)', content):
line_num = content[:match.start()].count('\n') + 1
start = content.rfind('\n', 0, match.start()) + 1
end = content.find('\n', match.start())
print(f"Line {line_num}: {content[start:end]}")
EOF


</function_calls>

Now let me check the structure more carefully:
<function_calls>

sed -n '191,280p' src/utils/pnl.ts

</function_calls>

<function_calls>

sed -n '280,353p' src/utils/pnl.ts

</function_calls>

Let me check if there's a calculatePnLFromRelay function and its USDC_ADDRESSES:
<function_calls>

sed -n '353,$p' src/utils/pnl.ts | head -300

</function_calls>

<function_calls>

grep -n "const calculatePnLFromRelay|export const calculatePnLFromRelay|function calculatePnLFromRelay" src/utils/pnl.ts

</function_calls>

Let me verify the file content and check if calculatePnLFromRelay exists:
<function_calls>

rg "calculatePnLFromRelay" src/

</function_calls>

<function_calls>

grep -r "USDC_ADDRESSES.*Arbitrum|USDC_ADDRESSES.*Optimism" src/

</function_calls>

Let me search more broadly:
<function_calls>

rg "0x.*// .*USDC" src/utils/pnl.ts -B 1 -A 1

</function_calls>

<function_calls>

cat src/utils/pnl.ts | grep -A 30 "getRelayValidatedTrades" | head -40

</function_calls>

Let me take a different approach - check the entire file structure:
<function_calls>

python3 << 'EOF'
with open('src/utils/pnl.ts', 'r') as f:
lines = f.readlines()

Print summary of functions and key elements

in_function = None
for i, line in enumerate(lines[190:354], start=191):
if 'export const' in line or 'const ' in line or 'USDC_ADDRESSES' in line or 'inTxs' in line or 'outTxs' in line:
print(f"{i}: {line.rstrip()}")
EOF


</function_calls>

<function_calls>

sed -n '220,250p' src/utils/pnl.ts

</function_calls>

<function_calls>

sed -n '250,280p' src/utils/pnl.ts

</function_calls>

Now let me confirm the review's claim about calculatePnLFromRelay by searching the entire codebase:
<function_calls>

find src -name ".ts" -o -name ".tsx" | xargs grep -l "calculatePnLFromRelay"

</function_calls>

<function_calls>

find src -name ".ts" -o -name ".tsx" | xargs grep -l "outTxs"

</function_calls>

Let me check if the review is referencing code that might not exist in this PR:
<function_calls>

rg "relayReq.data" src/utils/pnl.ts -B 2 -A 2

</function_calls>

<function_calls>

sed -n '191,353p' src/utils/pnl.ts | grep -n "inTxs|outTxs|USDC_ADDRESSES"

</function_calls>

Let me get the full content of the reviewed function:
<function_calls>

sed -n '191,353p' src/utils/pnl.ts > /tmp/reviewed_section.ts && wc -l /tmp/reviewed_section.ts

</function_calls>

<function_calls>

cat /tmp/reviewed_section.ts | tail -100

</function_calls>

Now let me verify the key claims in the review by examining what's actually at those line numbers:
<function_calls>

python3 << 'EOF'
with open('src/utils/pnl.ts', 'r') as f:
lines = f.readlines()

Check lines 191-353

section = lines[190:353]

Check for USDC_ADDRESSES

for i, line in enumerate(section, start=191):
if 'USDC_ADDRESSES' in line:
print(f"Line {i}: {line.rstrip()}")
# Print next 10 lines
for j in range(1, 11):
if 190 + i - 191 + j < len(lines):
print(f"Line {i+j}: {lines[190 + i - 191 + j].rstrip()}")
break

print("\n" + "="*80 + "\n")

Check for inTxs

for i, line in enumerate(section, start=191):
if 'inTxs' in line:
print(f"Line {i}: {line.rstrip()}")

print("\n" + "="*80 + "\n")

Check for outTxs

for i, line in enumerate(section, start=191):
if 'outTxs' in line:
print(f"Line {i}: {line.rstrip()}")
EOF


</function_calls>

<function_calls>

sed -n '235,245p' src/utils/pnl.ts

</function_calls>

Let me get a clearer picture:
<function_calls>

python3 << 'EOF'
with open('src/utils/pnl.ts', 'r') as f:
content = f.read()
lines = content.split('\n')

Find line 235 and nearby for USDC_ADDRESSES

for i in range(230, 250):
print(f"{i+1}: {lines[i]}")
EOF


</function_calls>

Based on my exploration, let me now search for the specific patterns mentioned in the review:
<function_calls>

grep -n "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913|0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" src/utils/pnl.ts

</function_calls>

<function_calls>

sed -n '230,280p' src/utils/pnl.ts

</function_calls>

<function_calls>

sed -n '290,320p' src/utils/pnl.ts

</function_calls>

Now let me check if there are any other functions dealing with Relay and USDC:
<function_calls>

rg -n "export const.*Relay|const.*Relay" src/utils/pnl.ts

</function_calls>

<function_calls>

python3 << 'EOF'
import re

with open('src/utils/pnl.ts', 'r') as f:
lines = f.readlines()

Find all function definitions

functions = []
for i, line in enumerate(lines):
if re.match(r'\s*(export\s+)?const\s+\w+\s*=\s*(async\s*)?(|function\s+\w+', line):
functions.append((i+1, line.strip()))

print("Functions in pnl.ts:")
for line_num, func_def in functions:
print(f"Line {line_num}: {func_def[:80]}")
EOF


</function_calls>

Now let me check if calculatePnLFromRelay exists anywhere in the codebase or if it's mentioned only in the review:
<function_calls>

git log --oneline -20

</function_calls>

<function_calls>

git diff HEAD^ src/utils/pnl.ts | grep -A 5 -B 5 "calculatePnLFromRelay|outTxs" | head -40

</function_calls>

Let me check what branch we're on and see if calculatePnLFromRelay might be added:
<function_calls>

git status

</function_calls>

<function_calls>

git log --all --pretty=format:"%h %s" | head -20

</function_calls>

Let me verify whether the review comment references code that doesn't actually exist:
<function_calls>

python3 << 'EOF'

Read the entire file and check key claims from the review

with open('src/utils/pnl.ts', 'r') as f:
content = f.read()

Check 1: Does calculatePnLFromRelay exist?

has_calculatePnLFromRelay = 'calculatePnLFromRelay' in content
print(f"calculatePnLFromRelay exists: {has_calculatePnLFromRelay}")

Check 2: Count USDC_ADDRESSES definitions

usdc_count = content.count('USDC_ADDRESSES')
print(f"USDC_ADDRESSES appears {usdc_count} times")

Check 3: Does getRelayValidatedTrades check outTxs?

getRelayValidatedTrades_idx = content.find('getRelayValidatedTrades')
if getRelayValidatedTrades_idx >= 0:
# Extract the function (estimate end by looking for next export const)
func_start = getRelayValidatedTrades_idx
next_export = content.find('export const', func_start + 1)
if next_export < 0:
next_export = len(content)
func_content = content[func_start:next_export]

has_outTxs_in_func = 'outTxs' in func_content
has_inTxs_in_func = 'inTxs' in func_content

print(f"In getRelayValidatedTrades:")
print(f"  - has inTxs: {has_inTxs_in_func}")
print(f"  - has outTxs: {has_outTxs_in_func}")

EOF


</function_calls>

Perfect! Now I have the key finding. Let me create one more verification to see the actual USDC_ADDRESSES list:
<function_calls>

python3 << 'EOF'
import re

with open('src/utils/pnl.ts', 'r') as f:
content = f.read()

Extract all USDC_ADDRESSES lists

pattern = r"USDC_ADDRESSES\s*=\s*[(.*?)]"
matches = list(re.finditer(pattern, content, re.DOTALL))

print(f"Found {len(matches)} USDC_ADDRESSES definitions:\n")

for i, match in enumerate(matches, 1):
line_num = content[:match.start()].count('\n') + 1
usdc_list = match.group(1)
# Count addresses in list
addr_count = usdc_list.count('0x')
print(f"Match {i} (line ~{line_num}): {addr_count} addresses")
print(usdc_list.strip()[:200] + "...\n")
EOF


</function_calls>

Now let me verify the specific addresses mentioned in the review:
<function_calls>

grep -n "// Base USDC|// Ethereum USDC|// Polygon USDC|// BSC USDC|// Arbitrum|// Optimism" src/utils/pnl.ts

</function_calls>

Perfect! Now I have enough information. Let me do a final check on the complete section to understand what's there:
<function_calls>

sed -n '191,353p' src/utils/pnl.ts | grep -n "USDC|inTxs|outTxs" | head -20

</function_calls>

Based on my verification, I can now draw conclusions:

Now let me search for any related code that might handle outTxs or other USDC addresses:
<function_calls>

rg "outTxs" src/ --type ts --type tsx

</function_calls>

<function_calls>

rg "Arbitrum.*USDC|Optimism.*USDC" src/ --type ts --type tsx

</function_calls>

Let me check the Relay types to understand the schema:
<function_calls>

find src -name ".ts" -o -name ".tsx" | xargs grep -l "RelayRequest|inTxs|outTxs" | head -5

</function_calls>

<function_calls>

rg "interface RelayRequest|type RelayRequest" src/ -A 20

</function_calls>

<function_calls>

rg "inTxs|outTxs" src/ -B 3 -A 3 | head -50

</function_calls>

Let me check if there are type definitions or imports that show the Relay schema:
<function_calls>

head -50 src/utils/pnl.ts

</function_calls>

<function_calls>

rg "type.=.{|interface" src/utils/pnl.ts | head -20

</function_calls>

Let me check related types in the types file:
<function_calls>

find src -name "*.ts" | xargs grep -l "RelayRequest" | head -3

</function_calls>

<function_calls>

rg "export.*RelayRequest|type RelayRequest" src/ -A 30 | head -80

</function_calls>

Based on my comprehensive verification, I can now provide my findings:

Verify review comment references non-existent calculatePnLFromRelay function

The review references calculatePnLFromRelay and its handling of both inTxs and outTxs, but this function does not exist in the codebase.

While the concern about getRelayValidatedTrades checking only relayReq.data?.inTxs (and not outTxs) is technically valid, the comparison to a hypothetical calculatePnLFromRelay with 6 supported chains cannot be verified. The claim about Arbitrum/Optimism USDC addresses being in a different list is unsubstantiated.

The identified issues remain valid as concerns:

  • USDC_ADDRESSES in getRelayValidatedTrades covers 4 chains only (Base, Ethereum, Polygon, BSC)
  • The function checks only inTxs and ignores outTxs

However, without evidence that the broader outTxs pattern or additional chains are actually needed for correctness, the review's framing is speculative. The recommendations should be rewritten to focus on actual code patterns in the repository.

- Implement PnL calculation using Relay API for accurate buy/sell tracking
- Add PnLStats component with 4-column layout
- Add TokenPnLCell component for portfolio list PnL display
- Create useTokenPnL hook for fetching and calculating PnL metrics
- Add auto-refresh (30s) and manual refresh support for PnL data
- Integrate PnL display in Buy and Sell components
- Add skeleton loading animations for PnL components
- Update My Holdings layout with improved column spacing and sort indicators
- Add Relay API service for fetching transaction data
- Add wallet transactions API integration
- Implement PnL utilities with test coverage

Refactored PnL utilities to comply with linting rules:
- Replaced for-of loops with Promise.all and array methods
- Eliminated continue statements using filter/map
- Added explicit return types and fixed hook dependencies
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Nov 26, 2025

Deploying pillarx-debug with  Cloudflare Pages  Cloudflare Pages

Latest commit: f2ce30b
Status: ✅  Deploy successful!
Preview URL: https://7609f21b.pillarx-debug.pages.dev
Branch Preview URL: https://feature-pnl.pillarx-debug.pages.dev

View logs

@github-actions github-actions bot requested a deployment to Preview (feature-pnl) November 26, 2025 09:57 Abandoned
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Nov 26, 2025

Deploying x with  Cloudflare Pages  Cloudflare Pages

Latest commit: f2ce30b
Status: ✅  Deploy successful!
Preview URL: https://37cd9dcc.x-e62.pages.dev
Branch Preview URL: https://feature-pnl.x-e62.pages.dev

View logs

@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) November 26, 2025 10:03 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

♻️ Duplicate comments (4)
inspect_aave_contracts_full.js (1)

1-21: Duplicate code: part of AAVE inspection script consolidation.

This is the third script with nearly identical logic to inspect_aave_portfolio.js and inspect_full_aave.js. Please see the consolidation suggestion in the review comment for inspect_full_aave.js.

src/apps/pulse/components/Search/PortfolioTokenList.tsx (2)

77-80: Debug console.log already flagged

This debug logging was previously flagged and should be removed before merging.


195-199: PnL sort no-op already flagged

The PnL sort returning 0 for both values was previously flagged.

src/apps/pulse/components/PnLStats/PnLStats.tsx (1)

150-156: Negative PnL sign still missing in USD formatting (previously flagged).

The past review correctly identified that negative USD values render without the - sign (e.g., $0.80 (-0.5%) instead of -$0.80 (-0.5%)). This occurs because Math.abs(usdValue) removes the sign, and the sign variable is empty string for negative values rather than -.

   const formatPnLWithPercent = (usdValue: number, pctValue: number) => {
-    const sign = usdValue >= 0 ? '+' : '';
-    const usdFormatted = `${sign}${formatUSD(Math.abs(usdValue))}`;
-    const pctFormatted = `${sign}${pctValue.toFixed(2)}%`;
+    const usdSign = usdValue > 0 ? '+' : usdValue < 0 ? '-' : '';
+    const pctSign = pctValue > 0 ? '+' : pctValue < 0 ? '-' : '';
+    const usdFormatted = `${usdSign}${formatUSD(Math.abs(usdValue))}`;
+    const pctFormatted = `${pctSign}${Math.abs(pctValue).toFixed(2)}%`;
     return `${usdFormatted} (${pctFormatted})`;
   };
🟡 Minor comments (11)
inspect_relay_full.js-1-9 (1)

1-9: Add error handling and consider consolidating Relay fetch scripts.

This script lacks error handling and duplicates logic found in other Relay-related scripts.

Apply this diff to add basic error handling:

 const fetch = require('node-fetch');

 (async () => {
     const hash = '0xad9cc422081297ddd3955b60a7573da4c6fc3086b4475b38af48ed25155c7ff6';
     console.log(`Fetching Relay data for ${hash}...`);
+    try {
         const response = await fetch(`https://api.relay.link/requests/v2?hash=${hash}`);
+        if (!response.ok) {
+            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+        }
         const data = await response.json();
         console.log(JSON.stringify(data, null, 2));
+    } catch (error) {
+        console.error('Failed to fetch Relay data:', error.message);
+        process.exit(1);
+    }
 })();

Additionally, consider consolidating with test_relay_by_user.js and debug_steth_tx.js into a single parameterized utility script.

debug_steth_tx.ts-3-3 (1)

3-3: Remove unused import.

calculatePnLFromRelay is imported but never used in this script.

 import { fetchRelayRequestByHash } from './src/services/relayApi';
-import { calculatePnLFromRelay } from './src/utils/pnl';
debug_aave.ts-77-78 (1)

77-78: Type the error parameter in catch block

Accessing e.message on an implicitly unknown type will fail in strict TypeScript:

-        } catch (e) {
-            console.log(`\n❌ Error querying Relay: ${e.message}`);
+        } catch (e) {
+            const message = e instanceof Error ? e.message : String(e);
+            console.log(`\n❌ Error querying Relay: ${message}`);
         }
verify_transactions.ts-49-49 (1)

49-49: Missing .catch() handler on script invocation.

Other scripts use .catch(console.error) to handle promise rejections. This script should follow the same pattern for consistency and to ensure errors are visible.

-verifyTransactions();
+verifyTransactions().catch(console.error);
test_portfolio_pnl.ts-74-77 (1)

74-77: Potential runtime error from undefined array access.

asset.asset.contracts[0] will throw if contracts is undefined or empty. Add a safety check.

-        const contract = asset.asset.contracts[0];
+        const contract = asset.asset.contracts?.[0];
         const price = asset.price;
         const usdValue = price * asset.token_balance;
verify_aave_pnl.js-38-47 (1)

38-47: Missing null check before accessing aaveAsset properties.

If AAVE is not found in the portfolio, aaveAsset will be undefined and line 39 will throw Cannot read properties of undefined.

     const aaveAsset = assets.find(a => a.asset.symbol === 'AAVE');
+    if (!aaveAsset) {
+        console.error('AAVE asset not found in portfolio!');
+        return;
+    }
     const aaveToken = aaveAsset.contracts_balances
         .filter(cb => cb.balance > 0)
test_relay_matching.ts-72-81 (1)

72-81: USDC transaction filter appears ineffective.

The usdcTxs filter on line 78 filters transactions where t.asset.symbol === 'USDC', but txs is already grouped by symbol from line 72. If symbol is 'USDC', all transactions match; otherwise, none do. This logic seems unintentional.

Did you mean to check for USDC involvement in the transaction pairs rather than filtering by the grouped symbol?

test_all_assets_pnl.js-163-167 (1)

163-167: Potential undefined access for asset.asset.contracts[0].

If asset.asset.contracts is an empty array, contracts[0] will be undefined, and the fallback to asset.asset.contract may also be undefined, causing issues downstream.

         const symbol = asset.asset.symbol;
-        const contract = asset.asset.contracts[0] || asset.asset.contract;
+        const contract = asset.asset.contracts?.[0] || asset.asset.contract;
+        if (!contract) {
+            console.log(`Skipping ${symbol}: no contract address`);
+            continue;
+        }
src/utils/pnl.ts-88-98 (1)

88-98: ReconstructedTrade uses feesUSD but the type definition uses fees.

The type ReconstructedTrade in src/types/api.ts (lines 802-812) defines fees?: number, but this code sets feesUSD. This will cause a type error or the field will be ignored.

     trades.push({
       side,
       txHash,
       timestamp,
       amountToken: absTokenChange,
       amountQuoteUSDC: absUsdcChange,
       execPriceUSD: absUsdcChange / absTokenChange,
-      feesUSD: feeUsd, // Not implemented fully yet as per complexity
+      fees: feeUsd, // Not implemented fully yet as per complexity
       tokenAddress: tokenAddress || '',
       tokenSymbol,
     });

The same issue exists on lines 352 and 500 (using feesUSD instead of fees).

test_complete_pnl_flow.js-1-3 (1)

1-3: Hardcoded wallet address should be externalized or removed before merge.

The wallet address on line 2 appears to be a real address. For a test script that may be committed, consider using environment variables or command-line arguments.

 const fetch = require('node-fetch');
-const WALLET = '0x70e8741c1758Ba32176B188286B8086956627B1c';
+const WALLET = process.env.TEST_WALLET || process.argv[2];
+
+if (!WALLET) {
+    console.error('Usage: node test_complete_pnl_flow.js <wallet_address>');
+    console.error('Or set TEST_WALLET environment variable');
+    process.exit(1);
+}
src/store.ts-35-47 (1)

35-47: Based on my verification, I can now provide the rewritten review comment:

Tighten addReducer typing to require at least one reducer identifier

The current type signature allows callers to pass an object with only reducer (missing both reducerPath and name), which defers validation to runtime. All five call sites in the codebase pass Redux Toolkit slices that include the name property, so adopting a discriminated union would be compatible with existing usage while preventing compile-time errors.

Use a discriminated union to enforce that at least one identifier is present:

-export const addReducer = (newReducer: {
-  reducerPath?: string;
-  name?: string;
-  reducer: Reducer;
-}) => {
+type DynamicReducerConfig<R extends Reducer = Reducer> =
+  | { reducerPath: string; reducer: R; name?: never }
+  | { name: string; reducer: R; reducerPath?: never };
+
+export const addReducer = (newReducer: DynamicReducerConfig) => {
   const path = newReducer.reducerPath || newReducer.name;
   if (!path) {
     console.error('Reducer path or name is missing', newReducer);
     return;
   }
   middlewareReducers[path] = newReducer.reducer;
   store.replaceReducer(combineReducers(middlewareReducers));
 };

This enforces at compile time that exactly one of reducerPath or name is provided, preventing the runtime error condition.

🧹 Nitpick comments (48)
test_relay_by_user.js (1)

1-27: Well-structured test script with proper error handling.

This script demonstrates good practices with comprehensive error handling and response structure inspection. However, the hardcoded wallet address should be parameterized.

Consider making the wallet address configurable:

 const fetch = require('node-fetch');
-const WALLET = '0x70e8741c1758Ba32176B188286B8086956627B1c';
+const WALLET = process.argv[2] || process.env.WALLET_ADDRESS || '0x70e8741c1758Ba32176B188286B8086956627B1c';

 (async () => {
     console.log('Testing Relay API fetch by user...');
+    console.log('Wallet:', WALLET);
     try {
         // ... rest of code
debug_steth_tx.ts (1)

22-32: Incomplete logic: clarify the purpose or complete the implementation.

This section contains incomplete/commented logic about testing PnL calculations. If this script is intended for debugging, consider either:

  1. Completing the PnL calculation test with actual token details
  2. Removing the incomplete section
  3. Moving this to a TODO or issue if the work is deferred

The comments suggest uncertainty about chain/token details. Consider adding a clear purpose statement at the top of the file explaining what this debug script validates.

debug_steth_tx.js (1)

1-41: Consider consolidating with TypeScript version or documenting the rationale.

This JavaScript version duplicates debug_steth_tx.ts and re-implements fetchRelayRequestByHash locally. While the comments (lines 6-7) explain this avoids TypeScript/ESM import issues, consider:

  1. If both versions are needed: Document in a README why both JS and TS versions exist
  2. If only one is needed: Remove the duplicate and use a simple build step for the TS version
  3. If this is temporary: Add a TODO comment indicating this should be consolidated

Additionally, the return structure at line 16 ({ data: data }) doesn't match the actual Relay API response structure. According to src/services/relayApi.ts, the API returns { requests: [...] }, and fetchRelayRequestByHash returns a single RelayRequest | null.

         const data = await response.json();
-        return { data: data }; // Mimic the structure expected
+        // Relay API v2 returns { requests: [...] }
+        const requests = data.requests || [];
+        return requests.length > 0 ? requests[0] : null;
     } catch (error) {
test_rtk_response.js (1)

1-59: Test script should not be committed to production codebase

This debug script contains a hardcoded wallet address and lacks error handling. Consider:

  1. Moving to a scripts/ or tests/ directory if needed for debugging
  2. Using environment variables for wallet addresses instead of hardcoding
  3. Adding error handling for network failures
+const WALLET = process.env.TEST_WALLET || '0x...';
+
 (async () => {
+  try {
     console.log('=== TESTING RTK QUERY RESPONSE STRUCTURE ===\n');
     // ... rest of the code
+  } catch (error) {
+    console.error('Error testing RTK response:', error.message);
+    process.exit(1);
+  }
 })();

If this is only for local debugging, consider adding it to .gitignore instead of committing.

src/apps/pulse/components/Search/PortfolioTokenList.tsx (1)

486-489: Remove commented-out code

This commented-out code block appears to be leftover from development. Remove it to keep the codebase clean:

                     ${/* // Skip PnL for small balances */}
-                    {/* if (valueUSD < 0.01) { */}
-                    {/*   return { ...token, pnl: null }; */}
-                    {/* } */}
                     {token.price
test_usdc_detection.js (3)

6-11: Centralize USDC_ADDRESSES constant

This constant is duplicated across multiple test files (test_complete_pnl_flow.js, test_optimized_pnl.js, and here). Extract it to a shared test utilities file or constants module to maintain a single source of truth.

// e.g., in test/constants.js or src/constants/tokens.ts
export const USDC_ADDRESSES = [
    '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // Base USDC
    '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // Ethereum USDC
    // ...
];

4-5: Avoid hardcoding wallet addresses and transaction hashes

Hardcoded credentials can be a privacy concern. Use environment variables or a test configuration file:

-const WALLET = '0x70e8741c1758Ba32176B188286B8086956627B1c';
-const TX_HASH = '0xad9cc422081297ddd3955b60a7573da4c6fc3086b4475b38af48ed25155c7ff6';
+const WALLET = process.env.TEST_WALLET;
+const TX_HASH = process.env.TEST_TX_HASH;
+
+if (!WALLET || !TX_HASH) {
+    console.error('Set TEST_WALLET and TEST_TX_HASH environment variables');
+    process.exit(1);
+}

13-40: Add error handling for network requests

The script lacks error handling for fetch failures, which could cause unhandled promise rejections:

 (async () => {
+  try {
     console.log('Testing USDC detection in Relay stateChanges\n');
     // ... existing code
+  } catch (error) {
+    console.error('Error:', error.message);
+    process.exit(1);
+  }
 })();
debug_aave.ts (2)

1-2: Use proper TypeScript import for node-fetch

Using @ts-ignore suppresses type checking. Since this is a TypeScript file, use a proper import:

-// @ts-ignore
-const fetch = require('node-fetch');
+import fetch from 'node-fetch';

Ensure @types/node-fetch is installed as a dev dependency if not already present.


4-6: Consider using environment variables for configuration

Similar to other test scripts, hardcoded URLs and wallet addresses could be moved to environment variables for flexibility:

-const WALLET = '0x70e8741c1758Ba32176B188286B8086956627B1c';
-const API_URL = 'https://hifidata-7eu4izffpa-uc.a.run.app';
-const RELAY_URL = 'https://api.relay.link';
+const WALLET = process.env.TEST_WALLET || '';
+const API_URL = process.env.API_URL || 'https://hifidata-7eu4izffpa-uc.a.run.app';
+const RELAY_URL = process.env.RELAY_URL || 'https://api.relay.link';
inspect_aave_contracts.js (2)

1-35: Consolidate debug scripts into a unified tool

This script follows the same pattern as test_rtk_response.js, test_usdc_detection.js, and debug_aave.ts. Consider:

  1. Creating a unified CLI tool with subcommands for different inspections
  2. Centralizing wallet address configuration
  3. Adding proper error handling
  4. Moving to a dedicated scripts/ directory
// Example: scripts/debug-cli.js
const commands = {
  'aave-contracts': inspectAaveContracts,
  'usdc-detection': testUsdcDetection,
  'rtk-response': testRtkResponse,
};

const command = process.argv[2];
if (!commands[command]) {
  console.log('Usage: node debug-cli.js <command>');
  process.exit(1);
}
commands[command]().catch(console.error);

This reduces duplication and provides a cleaner development experience.


4-35: Add error handling for async operations

Wrap the IIFE in try-catch to handle potential failures gracefully:

 (async () => {
+  try {
     console.log('Fetching transactions for wallet:', WALLET);
     // ... existing code
+  } catch (error) {
+    console.error('Failed to inspect AAVE contracts:', error.message);
+    process.exit(1);
+  }
 })();
find_hashes.ts (3)

1-6: Debug scripts should not reside in src/ or production directories.

These diagnostic scripts (find_hashes.ts, inspect_relay.ts, etc.) appear to be ad-hoc debugging tools with hardcoded wallet addresses and API URLs. Consider:

  1. Moving to a dedicated scripts/ or tools/ directory.
  2. Using environment variables for WALLET_ADDRESS and RELAY_API_URL.
  3. Adding these to .gitignore if they contain personal test data, or sanitizing the wallet address.
-const WALLET_ADDRESS = '0x70e8741c1758Ba32176B188286B8086956627B1c';
-const RELAY_API_URL = 'https://api.relay.link';
+const WALLET_ADDRESS = process.env.TEST_WALLET_ADDRESS || '';
+const RELAY_API_URL = process.env.RELAY_API_URL || 'https://api.relay.link';

7-12: Add error handling for fetch and response validation.

The fetch call lacks error handling for network failures and does not validate the response status before parsing JSON.

 async function findTransactionHashes() {
-    const relayResponse = await fetch(`${RELAY_API_URL}/requests?user=${WALLET_ADDRESS}&status=filled`);
-    const relayData = await relayResponse.json();
+    const relayResponse = await fetch(`${RELAY_API_URL}/requests?user=${WALLET_ADDRESS}&status=filled`);
+    if (!relayResponse.ok) {
+        throw new Error(`Relay API request failed: ${relayResponse.status}`);
+    }
+    const relayData = await relayResponse.json();
     const relayRequests = relayData?.requests || [];

21-37: Consider adding explicit types for the recursive function.

The findHashesRecursive function uses any type. For better maintainability, consider using a more descriptive type or unknown with type guards.

inspect_relay.ts (2)

1-6: Duplicated setup pattern across debug scripts.

This file shares the same hardcoded wallet address and API URL pattern as find_hashes.ts and other debug scripts. Consider creating a shared configuration module for these scripts to reduce duplication and make testing different wallets easier.


10-12: Missing response validation before JSON parsing.

Similar to find_hashes.ts, this lacks error handling for failed requests.

     const relayResponse = await fetch(`${RELAY_API_URL}/requests?user=${WALLET_ADDRESS}&status=filled`);
+    if (!relayResponse.ok) {
+        throw new Error(`Relay API request failed: ${relayResponse.status}`);
+    }
     const relayData = await relayResponse.json();
inspect_aave_hashes.js (2)

7-24: Missing error handling for API request.

The fetch call does not check resp.ok before parsing. If the API returns an error status, resp.json() may fail or return unexpected data.

     const resp = await fetch('https://hifidata-7eu4izffpa-uc.a.run.app?testnets=false', {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({
             path: 'wallet/transactions',
             params: {
                 wallet: WALLET,
                 limit: 500,
                 page: 1,
                 filterSpam: true,
             },
         }),
     });
 
+    if (!resp.ok) {
+        throw new Error(`API request failed: ${resp.status}`);
+    }
+
     const data = await resp.json();

31-35: Consider using Map for grouping consistency.

Other scripts in this PR use Map for grouping (e.g., test_final_matching.ts). Using a consistent approach across scripts improves maintainability.

test_final_matching.ts (2)

12-28: Add error handling for API requests.

Both the Mobula and Relay API calls lack response validation. Network failures or API errors will cause silent failures or confusing error messages.

     const mobResp = await fetch(`${API_URL}?testnets=false`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({
             path: 'wallet/transactions',
             params: { wallet: WALLET, limit: 500, page: 1, filterSpam: true },
         }),
     });
+    if (!mobResp.ok) {
+        throw new Error(`Mobula API request failed: ${mobResp.status}`);
+    }
     const mobData = await mobResp.json();
     const txs = mobData?.result?.data?.transactions || [];
     console.log(`✓ Fetched ${txs.length} Mobula transactions\n`);

     // 2. Fetch Relay requests
     const relayResp = await fetch(`${RELAY_URL}/requests?user=${WALLET}&status=filled`);
+    if (!relayResp.ok) {
+        throw new Error(`Relay API request failed: ${relayResp.status}`);
+    }
     const relayData = await relayResp.json();

61-65: O(n²) complexity for USDC involvement check.

The nested txs.some() inside the outer loop creates quadratic complexity. For large transaction sets, consider pre-building a hash-to-USDC lookup map.

+    // Pre-build USDC lookup by hash
+    const usdcByHash = new Set(
+        txs.filter(tx => tx.asset?.symbol === 'USDC').map(tx => tx.tx_hash)
+    );
+
     tokenMatches.forEach((data, symbol) => {
         // ...
         const hasUSDC = data.txs.some(t => {
-            const txHash = t.tx_hash;
-            return txs.some(tx2 => tx2.tx_hash === txHash && tx2.asset.symbol === 'USDC');
+            return usdcByHash.has(t.tx_hash);
         });
test_pnl_calculation.ts (3)

9-32: Duplicates logic from src/utils/pnl.ts.

This local calculatePnL function duplicates the core PnL calculation logic already implemented in src/utils/pnl.ts. The production version is more comprehensive (includes avgBuyPrice, avgSellPrice, realisedPnLPct, etc.). Consider importing and reusing the existing utility.

+import { calculatePnL } from './utils/pnl';
+
-// Simple PnL calculation
-function calculatePnL(trades, currentPrice) {
-    // ... local implementation
-}

Based on the relevant code snippet from src/utils/pnl.ts, the production function returns a richer PnLMetrics object that this test could validate against.


73-110: N+1 query pattern and empty catch block.

Two concerns:

  1. N+1 queries: Each unique hash triggers a separate Relay API call. For wallets with many transactions, this could be slow and risk rate limiting. Consider batching or using the user-based endpoint with local filtering.

  2. Silent error swallowing: The empty catch block (lines 107-109) hides all errors, making debugging difficult.

             } catch (e) {
-                // 404 or error = not in Relay
+                // Log non-404 errors for debugging
+                if (e.response?.status !== 404) {
+                    console.warn(`Relay lookup failed for ${hash}:`, e.message);
+                }
             }

38-48: Missing response validation for Mobula API call.

Consistent with other files, add response status check before parsing JSON.

     const mobResp = await fetch(`${API_URL}?testnets=false`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({
             path: 'wallet/transactions',
             params: { wallet: WALLET, limit: 500, page: 1, filterSpam: true },
         }),
     });
+    if (!mobResp.ok) {
+        throw new Error(`Mobula API request failed: ${mobResp.status}`);
+    }
     const mobData = await mobResp.json();
validate_pnl.js (4)

4-6: Hardcoded test credentials should be documented or parameterized.

Consider accepting the wallet address as a command-line argument or environment variable for flexibility when testing different wallets. At minimum, add a comment indicating this is a test-only value.

+// Test wallet address - change as needed for validation
 const WALLET = '0x70e8741c1758Ba32176B188286B8086956627B1c';

9-33: Duplicated calculatePnL function across test scripts.

This function is duplicated in validate_pnl.js, test_portfolio_pnl.ts, and test_optimized_pnl.js. The production version exists in src/utils/pnl.ts. For test scripts, consider importing from the production module to ensure consistency and reduce maintenance burden.

-// Exact PnL calculation from the codebase
-function calculatePnL(trades, currentPrice) {
-    let totalTokens = 0;
-    let totalCostUSDC = 0;
-    // ... function body
-}
+const { calculatePnL } = require('./src/utils/pnl');

43-43: Silent error swallowing hides debugging information.

While tolerant error handling is appropriate for this validation script, completely empty catch blocks make debugging difficult. Consider logging at debug level.

-    } catch (e) { }
+    } catch (e) {
+        // Silently skip - hash may not exist in Relay
+    }

51-59: Missing error handling for API response failures.

If the API returns an error status or malformed JSON, the script will throw an unhandled exception. Consider adding response status checks.

     const txResp = await fetch(`${API_URL}?testnets=false`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({
             path: 'wallet/transactions',
             params: { wallet: WALLET, limit: 500, page: 1, filterSpam: true },
         }),
     });
+    if (!txResp.ok) {
+        throw new Error(`Transaction API failed: ${txResp.status}`);
+    }
     const txData = await txResp.json();
test_portfolio_pnl.ts (1)

1-2: TypeScript file lacks type annotations.

This is a .ts file but uses @ts-ignore and no type annotations. Either convert to plain JavaScript or add proper types to leverage TypeScript's safety benefits.

test_optimized_pnl.js (2)

8-13: USDC addresses duplicated across multiple test files.

This array is defined identically in test_all_assets_pnl.js, verify_hybrid_pnl.js, test_complete_pnl_flow.js, and test_usdc_detection.js. Consider extracting to a shared constants file.


86-121: Sequential API calls without rate limiting may hit Relay API limits.

Querying Relay for each hash sequentially without any delay could result in rate limiting for wallets with many transactions. Consider adding a small delay or using batch requests if supported.

     for (const hash of uniqueHashes) {
         try {
+            // Small delay to avoid rate limiting
+            await new Promise(resolve => setTimeout(resolve, 50));
             const relayResp = await fetch(`${RELAY_URL}/requests/v2?hash=${hash}`);
test_relay_pnl.ts (1)

40-47: Excessive use of any type throughout the file.

The file is TypeScript but relies heavily on any types, which provides no type safety. Since related types like MobulaTransactionRow and RelayRequest exist in the codebase (per the AI summary), consider importing and using them.

Also applies to: 75-77

test_relay_matching.ts (2)

4-6: Hardcoded wallet address and API URLs in committed test script.

Consider moving the wallet address and API URLs to environment variables or a configuration file. Hardcoded wallet addresses in source control can be a privacy concern, even in test scripts.

-const WALLET_ADDRESS = '0x70e8741c1758Ba32176B188286B8086956627B1c';
-const API_URL = 'https://hifidata-7eu4izffpa-uc.a.run.app';
-const RELAY_API_URL = 'https://api.relay.link';
+const WALLET_ADDRESS = process.env.TEST_WALLET_ADDRESS || '';
+const API_URL = process.env.HIFI_API_URL || 'https://hifidata-7eu4izffpa-uc.a.run.app';
+const RELAY_API_URL = process.env.RELAY_API_URL || 'https://api.relay.link';

12-28: Add error handling for API fetch calls.

Both fetch calls lack error handling. If either API is unavailable or returns an error, the script will crash with an unhelpful error message. Consider wrapping in try/catch or checking response status.

     // 1. Fetch Mobula transactions
-    const mobulaResponse = await fetch(`${API_URL}?testnets=false`, {
+    const mobulaResponse = await fetch(`${API_URL}?testnets=false`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({
             path: 'wallet/transactions',
             params: { wallet: WALLET_ADDRESS, limit: 500, page: 1, filterSpam: true },
         }),
     });
+    if (!mobulaResponse.ok) {
+        console.error(`Mobula API error: ${mobulaResponse.status}`);
+        return;
+    }
src/services/relayApi.ts (1)

130-131: Add blank line between functions for consistency.

There's a missing blank line between fetchRelayRequestByHash and fetchRelayRequestsByUser, unlike the spacing between other functions.

     return null;
   }
 };
+
 export const fetchRelayRequestsByUser = async (
verify_hybrid_pnl.js (1)

91-96: USDC_ADDRESSES constant duplicated across multiple files.

This same array appears in test_complete_pnl_flow.js, test_all_assets_pnl.js, and here. Consider extracting to a shared constants file to avoid maintenance burden and potential inconsistencies.

test_all_assets_pnl.js (2)

15-39: Local calculatePnL implementation differs from production version.

This local implementation omits realised PnL tracking that exists in src/utils/pnl.ts (lines 103-179). While acceptable for a test script, consider importing the production function to ensure test behavior matches actual app behavior.

+// Import production calculatePnL for consistent behavior
+// const { calculatePnL } = require('./src/utils/pnl');
+
 function calculatePnL(trades, currentPrice) {

113-115: Silent catch block swallows errors without logging.

The empty catch block makes it impossible to debug issues with individual Relay API calls. Consider at least logging errors or tracking failure counts.

         } catch (e) {
-            // Skip on error
+            console.debug(`Relay fetch failed for ${txHash}:`, e.message);
         }
src/hooks/useTokenPnL.ts (5)

29-52: Storing refetch in state creates unnecessary re-renders and circular updates.

The pattern of storing refetch inside the result state and then updating it via a separate useEffect is convoluted. The refetch function reference changes on every render because setResult in line 45 creates a new closure, causing the effect on line 49-52 to run repeatedly.

Consider a simpler approach that avoids storing refetch in state:

-  const [result, setResult] = useState<TokenPnLResult>({
-    pnl: null,
-    isLoading: false,
-    refetch: () => {},
-    debug: {
-      mobulaTxCount: 0,
-      relayRequestCount: 0,
-      relayResponseCount: 0,
-      status: 'Idle',
-    },
-  });
+  const [pnlState, setPnlState] = useState<{
+    pnl: PnLMetrics | null;
+    isLoading: boolean;
+    debug: TokenPnLResult['debug'];
+  }>({
+    pnl: null,
+    isLoading: false,
+    debug: {
+      mobulaTxCount: 0,
+      relayRequestCount: 0,
+      relayResponseCount: 0,
+      status: 'Idle',
+    },
+  });

   const [refreshTrigger, setRefreshTrigger] = useState(0);

   const refetch = useCallback(() => {
-    setResult((prev) => ({ ...prev, isLoading: true }));
+    setPnlState((prev) => ({ ...prev, isLoading: true }));
     setRefreshTrigger((prev) => prev + 1);
   }, []);

-  useEffect(() => {
-    // Update refetch function in result
-    setResult((prev) => ({ ...prev, refetch }));
-  }, [refetch]);
+
+  // Return combined result at end of hook
+  return { ...pnlState, refetch };

222-226: Cleanup function returns undefined unnecessarily.

The cleanup function in the useEffect should simply set isMounted = false without returning anything. React cleanup functions don't need an explicit return value.

     return () => {
       isMounted = false;
-      return undefined;
     };
-  }, [props, refreshTrigger]);
+  }, [props, refreshTrigger]);

148-158: Sequential API calls in Promise.all may overwhelm the Relay API.

While you limit to 20 hashes, firing all requests simultaneously could trigger rate limiting. Consider adding a small delay between batches or implementing proper retry logic with exponential backoff.

-        const relayResults = await Promise.all(
-          hashesToFetch.map(async (hash) => {
-            try {
-              const req = await fetchRelayRequestByHash(hash);
-              return req;
-            } catch (e) {
-              console.error(`Failed to fetch Relay for ${hash}:`, e);
-              return null;
-            }
-          })
-        );
+        // Process in batches of 5 to avoid rate limiting
+        const BATCH_SIZE = 5;
+        const relayResults: (RelayRequest | null)[] = [];
+        
+        for (let i = 0; i < hashesToFetch.length; i += BATCH_SIZE) {
+          const batch = hashesToFetch.slice(i, i + BATCH_SIZE);
+          const batchResults = await Promise.all(
+            batch.map(async (hash) => {
+              try {
+                return await fetchRelayRequestByHash(hash);
+              } catch (e) {
+                console.error(`Failed to fetch Relay for ${hash}:`, e);
+                return null;
+              }
+            })
+          );
+          relayResults.push(...batchResults);
+          if (!isMounted) return undefined;
+        }

160-164: Redundant isMounted checks after awaiting Promise.all.

Lines 160 and 164 both check isMounted and return early. The second check on line 164 is redundant since no async operation occurs between them.

         if (!isMounted) return undefined;

         relayRequests.push(...relayResults.filter((req) => req !== null));

-        if (!isMounted) return undefined;
-
         if (isMounted)

228-238: Auto-refresh interval may cause unnecessary API calls when tab is not visible.

The 30-second interval runs regardless of whether the user is viewing the page, potentially wasting API quota.

Consider pausing the interval when the document is hidden:

   useEffect(() => {
     if (!props) return undefined;

+    let interval: NodeJS.Timeout;
+    
+    const startInterval = () => {
+      interval = setInterval(() => {
+        setResult((prev) => ({ ...prev, isLoading: true }));
+        setRefreshTrigger((prev) => prev + 1);
+      }, 30000);
+    };
+    
+    const handleVisibilityChange = () => {
+      if (document.hidden) {
+        clearInterval(interval);
+      } else {
+        startInterval();
+      }
+    };
+    
+    startInterval();
+    document.addEventListener('visibilitychange', handleVisibilityChange);
-    const interval = setInterval(() => {
-      setResult((prev) => ({ ...prev, isLoading: true }));
-      setRefreshTrigger((prev) => prev + 1);
-    }, 30000); // 30 seconds

-    return () => clearInterval(interval);
+    return () => {
+      clearInterval(interval);
+      document.removeEventListener('visibilitychange', handleVisibilityChange);
+    };
   }, [props]);
test_complete_pnl_flow.js (3)

19-136: Duplicated implementation of calculatePnLFromRelay.

This function duplicates the logic from src/utils/pnl.ts (lines 366-507). Code duplication increases maintenance burden and risks divergence.

For a test script, consider importing from the source:

// If using ES modules or transpilation:
// import { calculatePnLFromRelay } from './src/utils/pnl';

// Or use a shared test utilities approach

If this script must remain standalone (e.g., for quick debugging without build), add a comment explaining why duplication is intentional.


278-285: Race condition when pushing to shared array in parallel.

Multiple async callbacks are pushing to relayRequests concurrently inside Promise.all. While JavaScript's single-threaded nature prevents data corruption, the order of insertions is non-deterministic. This may not matter for this script, but be aware if order is important.

         await Promise.all(
             batch.map(async (hash) => {
                 const req = await fetchRelayRequestByHash(hash);
-                if (req) {
-                    relayRequests.push(req);
-                }
+                return req;
             })
-        );
+        ).then(results => {
+            relayRequests.push(...results.filter(Boolean));
+        });

171-224: No error handling for main async flow.

If any API call fails (e.g., network error on portfolio fetch), the script will throw an unhandled promise rejection. Consider wrapping the main flow in try-catch.

 (async () => {
+  try {
     console.log('=== COMPLETE PNL FLOW TEST ===\n');
     // ... rest of the code ...
     console.log('\n✓ Test Complete!');
+  } catch (error) {
+    console.error('\n✗ Test failed:', error.message);
+    process.exit(1);
+  }
 })();
src/utils/pnl.ts (2)

44-47: Native token transactions are silently skipped without tracking gas fees.

The comment suggests gas fees should be tracked, but the current implementation discards all native token transactions. This could affect PnL accuracy for trades where significant gas was spent.

Consider at least logging or accumulating gas costs for future implementation:

       if (tx.type === 'native') {
-        // Gas fee usually
-        return;
+        // TODO: Track gas fees for more accurate PnL
+        // feeUsd += tx.amount_usd || 0;
+        return;
       }

Also applies to: 49-56


182-360: getRelayValidatedTrades makes unbounded parallel API calls.

Unlike useTokenPnL which limits to 20 hashes, this function fetches all unique transaction hashes in parallel (line 227-233). For wallets with many transactions, this could trigger rate limiting or cause performance issues.

Consider adding a limit or batching:

   const txHashes = Object.keys(groupedByTxHash);
+  const MAX_RELAY_REQUESTS = 50;
+  const limitedHashes = txHashes.slice(0, MAX_RELAY_REQUESTS);

   // Fetch all relay requests in parallel
-  const relayRequestPromises = txHashes.map(async (txHash) => {
+  const relayRequestPromises = limitedHashes.map(async (txHash) => {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 098d813 and 380aff8.

⛔ Files ignored due to path filters (1)
  • src/apps/pulse/components/Search/tests/__snapshots__/PortfolioTokenList.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (56)
  • debug_aave.ts (1 hunks)
  • debug_steth_tx.js (1 hunks)
  • debug_steth_tx.ts (1 hunks)
  • find_hashes.ts (1 hunks)
  • inspect_aave_contracts.js (1 hunks)
  • inspect_aave_contracts_full.js (1 hunks)
  • inspect_aave_hashes.js (1 hunks)
  • inspect_aave_portfolio.js (1 hunks)
  • inspect_full_aave.js (1 hunks)
  • inspect_relay.ts (1 hunks)
  • inspect_relay_full.js (1 hunks)
  • src/apps/key-wallet/components/AssetRow.tsx (0 hunks)
  • src/apps/key-wallet/components/AssetsList.tsx (6 hunks)
  • src/apps/key-wallet/components/SearchAssets.tsx (1 hunks)
  • src/apps/key-wallet/components/SendAssetModal.tsx (4 hunks)
  • src/apps/key-wallet/components/TransactionStatus.tsx (0 hunks)
  • src/apps/key-wallet/components/WalletAddress.tsx (1 hunks)
  • src/apps/key-wallet/components/__tests__/AssetRow.test.tsx (2 hunks)
  • src/apps/key-wallet/components/__tests__/SearchAssets.test.tsx (7 hunks)
  • src/apps/key-wallet/components/__tests__/SendAssetModal.test.tsx (3 hunks)
  • src/apps/key-wallet/components/__tests__/TransactionStatus.test.tsx (11 hunks)
  • src/apps/key-wallet/components/__tests__/WalletAddress.test.tsx (6 hunks)
  • src/apps/key-wallet/index.tsx (6 hunks)
  • src/apps/key-wallet/manifest.json (0 hunks)
  • src/apps/key-wallet/styles/tailwindKeyWallet.css (1 hunks)
  • src/apps/key-wallet/tailwind.keywallet.config.js (1 hunks)
  • src/apps/key-wallet/types/index.ts (0 hunks)
  • src/apps/key-wallet/utils/__tests__/blockchain.test.ts (9 hunks)
  • src/apps/key-wallet/utils/blockchain.ts (4 hunks)
  • src/apps/key-wallet/utils/portfolio.ts (0 hunks)
  • src/apps/pulse/components/PnLStats/PnLStats.tsx (1 hunks)
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx (5 hunks)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx (1 hunks)
  • src/apps/token-atlas/components/TokenGraphColumn/TokenGraphColumn.tsx (2 hunks)
  • src/hooks/useTokenPnL.ts (1 hunks)
  • src/services/pillarXApiWalletTransactions.ts (1 hunks)
  • src/services/relayApi.ts (1 hunks)
  • src/store.ts (4 hunks)
  • src/types/api.ts (1 hunks)
  • src/utils/pnl.test.ts (1 hunks)
  • src/utils/pnl.ts (1 hunks)
  • test_all_assets_pnl.js (1 hunks)
  • test_complete_pnl_flow.js (1 hunks)
  • test_final_matching.ts (1 hunks)
  • test_optimized_pnl.js (1 hunks)
  • test_pnl_calculation.ts (1 hunks)
  • test_portfolio_pnl.ts (1 hunks)
  • test_relay_by_user.js (1 hunks)
  • test_relay_matching.ts (1 hunks)
  • test_relay_pnl.ts (1 hunks)
  • test_rtk_response.js (1 hunks)
  • test_usdc_detection.js (1 hunks)
  • validate_pnl.js (1 hunks)
  • verify_aave_pnl.js (1 hunks)
  • verify_hybrid_pnl.js (1 hunks)
  • verify_transactions.ts (1 hunks)
💤 Files with no reviewable changes (5)
  • src/apps/key-wallet/components/AssetRow.tsx
  • src/apps/key-wallet/utils/portfolio.ts
  • src/apps/key-wallet/types/index.ts
  • src/apps/key-wallet/manifest.json
  • src/apps/key-wallet/components/TransactionStatus.tsx
✅ Files skipped from review due to trivial changes (11)
  • src/apps/key-wallet/components/tests/AssetRow.test.tsx
  • src/apps/key-wallet/components/tests/SearchAssets.test.tsx
  • src/apps/key-wallet/tailwind.keywallet.config.js
  • src/apps/key-wallet/components/WalletAddress.tsx
  • src/apps/key-wallet/utils/tests/blockchain.test.ts
  • src/apps/key-wallet/components/tests/WalletAddress.test.tsx
  • src/apps/key-wallet/components/tests/SendAssetModal.test.tsx
  • src/apps/key-wallet/components/SearchAssets.tsx
  • src/apps/key-wallet/index.tsx
  • src/apps/key-wallet/components/AssetsList.tsx
  • src/apps/key-wallet/styles/tailwindKeyWallet.css
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/services/pillarXApiWalletTransactions.ts
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/apps/token-atlas/components/TokenGraphColumn/TokenGraphColumn.tsx
  • src/types/api.ts
  • src/utils/pnl.test.ts
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/store.ts
  • src/apps/key-wallet/components/SendAssetModal.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/PnLStats/PnLStats.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/PnLStats/PnLStats.tsx
📚 Learning: 2025-11-04T14:34:00.373Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 445
File: src/containers/Authorized.tsx:63-77
Timestamp: 2025-11-04T14:34:00.373Z
Learning: In src/containers/Authorized.tsx, the tempKit useEffect intentionally excludes chainId from its dependency array because the kit is used by useWalletModeVerification to check wallet mode across all supported chains, regardless of the currently selected chainId. The chainId parameter is only used for initial kit configuration and does not affect the multi-chain verification results.

Applied to files:

  • src/apps/key-wallet/components/SendAssetModal.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/hooks/useTokenPnL.ts
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required text field. The TileTitleProps interface aligns with the TokensMarketData.title type in api.ts which also has optional fields.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required title field. The TileTitleProps structure aligns with how it's used in the TokensMarketData type in api.ts.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
🧬 Code graph analysis (26)
test_usdc_detection.js (2)
test_complete_pnl_flow.js (1)
  • USDC_ADDRESSES (23-30)
test_optimized_pnl.js (1)
  • USDC_ADDRESSES (8-13)
debug_steth_tx.ts (2)
verify_hybrid_pnl.js (1)
  • req (78-78)
src/services/relayApi.ts (1)
  • fetchRelayRequestByHash (107-130)
inspect_aave_contracts_full.js (1)
inspect_aave_portfolio.js (6)
  • fetch (1-1)
  • WALLET (2-2)
  • resp (5-12)
  • data (13-13)
  • assets (14-14)
  • aave (16-16)
src/store.ts (1)
src/services/pillarXApiWalletTransactions.ts (2)
  • pillarXApiWalletTransactions (21-49)
  • pillarXApiWalletTransactions (51-51)
src/apps/key-wallet/components/__tests__/TransactionStatus.test.tsx (1)
src/apps/key-wallet/types/index.ts (1)
  • TransactionStatus (21-26)
find_hashes.ts (1)
test_relay_by_user.js (1)
  • fetch (1-1)
test_final_matching.ts (4)
inspect_aave_contracts.js (3)
  • fetch (1-1)
  • txs (22-22)
  • data (21-21)
test_usdc_detection.js (4)
  • fetch (2-2)
  • relayResp (16-16)
  • relayData (17-17)
  • hasUSDC (22-22)
validate_pnl.js (3)
  • fetch (2-2)
  • data (39-39)
  • hasUSDC (92-92)
verify_hybrid_pnl.js (3)
  • fetch (1-1)
  • req (78-78)
  • data (8-8)
inspect_full_aave.js (2)
inspect_aave_contracts_full.js (6)
  • fetch (1-1)
  • WALLET (2-2)
  • resp (5-12)
  • data (13-13)
  • assets (14-14)
  • aave (16-16)
inspect_aave_portfolio.js (6)
  • fetch (1-1)
  • WALLET (2-2)
  • resp (5-12)
  • data (13-13)
  • assets (14-14)
  • aave (16-16)
inspect_aave_hashes.js (8)
inspect_aave_contracts.js (6)
  • fetch (1-1)
  • WALLET (2-2)
  • resp (7-19)
  • data (21-21)
  • txs (22-22)
  • aaveTxs (24-24)
inspect_full_aave.js (4)
  • fetch (1-1)
  • WALLET (2-2)
  • resp (5-12)
  • data (13-13)
inspect_relay_full.js (2)
  • fetch (1-1)
  • data (7-7)
test_all_assets_pnl.js (3)
  • fetch (2-2)
  • WALLET (4-4)
  • group (90-90)
test_complete_pnl_flow.js (4)
  • fetch (1-1)
  • WALLET (2-2)
  • data (8-8)
  • i (274-274)
test_relay_by_user.js (4)
  • fetch (1-1)
  • WALLET (2-2)
  • resp (7-7)
  • data (10-10)
validate_pnl.js (4)
  • fetch (2-2)
  • WALLET (4-4)
  • data (39-39)
  • aaveTxs (78-78)
verify_aave_pnl.js (3)
  • fetch (1-1)
  • WALLET (2-2)
  • data (9-9)
test_relay_matching.ts (3)
test_all_assets_pnl.js (2)
  • fetch (2-2)
  • API_URL (5-5)
test_relay_by_user.js (1)
  • fetch (1-1)
test_usdc_detection.js (1)
  • fetch (2-2)
src/apps/pulse/components/PnLStats/PnLStats.tsx (5)
src/types/api.ts (1)
  • PnLMetrics (815-826)
test_all_assets_pnl.js (3)
  • usdValue (167-167)
  • unrealisedPnLUSDC (35-35)
  • unrealisedPnLPct (36-36)
test_optimized_pnl.js (3)
  • usdValue (134-134)
  • unrealisedPnLUSDC (35-35)
  • unrealisedPnLPct (36-36)
test_complete_pnl_flow.js (2)
  • unrealisedPnLUSDC (157-157)
  • unrealisedPnLPct (158-158)
validate_pnl.js (2)
  • unrealisedPnLUSDC (29-29)
  • unrealisedPnLPct (30-30)
inspect_relay.ts (16)
debug_steth_tx.js (1)
  • fetch (2-2)
inspect_aave_contracts.js (1)
  • fetch (1-1)
inspect_aave_contracts_full.js (1)
  • fetch (1-1)
inspect_aave_hashes.js (1)
  • fetch (1-1)
inspect_aave_portfolio.js (1)
  • fetch (1-1)
inspect_full_aave.js (1)
  • fetch (1-1)
inspect_relay_full.js (1)
  • fetch (1-1)
test_all_assets_pnl.js (1)
  • fetch (2-2)
test_complete_pnl_flow.js (2)
  • fetch (1-1)
  • i (274-274)
test_optimized_pnl.js (1)
  • fetch (2-2)
test_relay_by_user.js (1)
  • fetch (1-1)
test_rtk_response.js (1)
  • fetch (1-1)
test_usdc_detection.js (1)
  • fetch (2-2)
validate_pnl.js (1)
  • fetch (2-2)
verify_aave_pnl.js (1)
  • fetch (1-1)
verify_hybrid_pnl.js (1)
  • fetch (1-1)
src/services/relayApi.ts (7)
test_complete_pnl_flow.js (2)
  • userAddress (70-70)
  • response (6-6)
verify_aave_pnl.js (2)
  • userAddress (102-102)
  • response (7-7)
verify_hybrid_pnl.js (2)
  • userAddress (144-144)
  • response (6-6)
debug_steth_tx.js (1)
  • response (10-10)
inspect_relay_full.js (1)
  • response (6-6)
test_rtk_response.js (1)
  • response (8-15)
validate_pnl.js (2)
  • response (37-37)
  • requests (40-40)
inspect_aave_portfolio.js (2)
inspect_aave_contracts_full.js (6)
  • fetch (1-1)
  • WALLET (2-2)
  • resp (5-12)
  • data (13-13)
  • assets (14-14)
  • aave (16-16)
inspect_full_aave.js (6)
  • fetch (1-1)
  • WALLET (2-2)
  • resp (5-12)
  • data (13-13)
  • assets (14-14)
  • aave (16-16)
inspect_relay_full.js (3)
test_complete_pnl_flow.js (3)
  • fetch (1-1)
  • response (6-6)
  • data (8-8)
test_relay_by_user.js (2)
  • fetch (1-1)
  • data (10-10)
verify_aave_pnl.js (3)
  • fetch (1-1)
  • response (7-7)
  • data (9-9)
src/apps/key-wallet/components/SendAssetModal.tsx (1)
src/types/walletProvider.ts (1)
  • WalletProviderLike (15-15)
test_all_assets_pnl.js (3)
test_complete_pnl_flow.js (7)
  • USDC_ADDRESSES (23-30)
  • trades (20-20)
  • trades (295-295)
  • unrealisedPnLUSDC (157-157)
  • unrealisedPnLPct (158-158)
  • side (35-35)
  • pnl (304-304)
verify_hybrid_pnl.js (3)
  • USDC_ADDRESSES (91-96)
  • trades (89-89)
  • side (101-101)
src/utils/pnl.ts (1)
  • calculatePnL (104-180)
src/apps/key-wallet/utils/blockchain.ts (2)
src/types/walletProvider.ts (1)
  • Eip1193LikeProvider (8-13)
src/utils/blockchain.ts (1)
  • getBlockScan (225-246)
test_pnl_calculation.ts (2)
test_relay_by_user.js (1)
  • WALLET (2-2)
src/utils/pnl.ts (1)
  • calculatePnL (104-180)
test_complete_pnl_flow.js (2)
src/services/relayApi.ts (1)
  • fetchRelayRequestByHash (107-130)
src/utils/pnl.ts (1)
  • calculatePnLFromRelay (366-508)
test_optimized_pnl.js (4)
test_all_assets_pnl.js (9)
  • USDC_ADDRESSES (8-13)
  • trades (42-42)
  • unrealisedPnLUSDC (35-35)
  • unrealisedPnLPct (36-36)
  • assets (134-134)
  • addr (76-76)
  • price (165-165)
  • balance (166-166)
  • pnl (177-177)
test_complete_pnl_flow.js (7)
  • USDC_ADDRESSES (23-30)
  • trades (20-20)
  • trades (295-295)
  • unrealisedPnLUSDC (157-157)
  • unrealisedPnLPct (158-158)
  • assets (186-186)
  • pnl (304-304)
test_usdc_detection.js (1)
  • USDC_ADDRESSES (6-11)
verify_hybrid_pnl.js (3)
  • USDC_ADDRESSES (91-96)
  • trades (89-89)
  • assets (32-32)
verify_transactions.ts (2)
test_relay_by_user.js (1)
  • fetch (1-1)
test_usdc_detection.js (1)
  • fetch (2-2)
src/utils/pnl.ts (3)
src/types/api.ts (3)
  • MobulaTransactionRow (768-790)
  • ReconstructedTrade (803-813)
  • PnLMetrics (815-826)
src/services/relayApi.ts (2)
  • RelayRequest (3-77)
  • fetchRelayRequestByHash (107-130)
test_usdc_detection.js (2)
  • USDC_ADDRESSES (6-11)
  • tokenAddr (28-28)
verify_aave_pnl.js (1)
src/services/relayApi.ts (1)
  • fetchRelayRequestByHash (107-130)
src/hooks/useTokenPnL.ts (4)
src/services/tokensData.ts (1)
  • Token (19-29)
src/types/api.ts (2)
  • WalletTransactionsMobulaResponse (792-801)
  • PnLMetrics (815-826)
src/utils/pnl.ts (2)
  • calculatePnL (104-180)
  • calculatePnLFromRelay (366-508)
src/services/relayApi.ts (2)
  • RelayRequest (3-77)
  • fetchRelayRequestByHash (107-130)
debug_steth_tx.js (1)
src/services/relayApi.ts (1)
  • fetchRelayRequestByHash (107-130)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug

Destructured props and updated useEffect dependency array to use specific
stable values (token.contract, walletAddress, etc.) instead of the unstable
props object. This prevents infinite re-rendering loops caused by
TokenPnLCell creating a new props object on every render. Also fixed a
TypeScript error by using token.contract instead of token.address.
@github-actions github-actions bot requested a deployment to Preview (feature-pnl) November 26, 2025 10:23 Abandoned
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/hooks/useTokenPnL.ts (1)

241-251: Dependency array still causes excessive re-renders.

While this approach improves on using the entire props object, line 247 still includes props.transactionsData, which is a full object reference. This will trigger the effect whenever transactionsData reference changes, even if its content is identical—potentially causing excessive API calls.

Destructure at the effect's start and use stable identifiers:

   }, [
-    // Use specific dependencies to avoid infinite loops from unstable props object
-    props ? props.token.contract : null,
-    props ? props.token.symbol : null,
-    props ? props.token.balance : null,
-    props ? props.token.price : null,
-    props ? props.transactionsData : null,
-    props ? props.walletAddress : null,
-    props ? props.chainId : null,
-    refreshTrigger
+    props?.token.contract,
+    props?.token.symbol,
+    props?.token.balance,
+    props?.token.price,
+    // Only depend on transaction count, not the entire object
+    props?.transactionsData?.data?.transactions?.length,
+    props?.walletAddress,
+    props?.chainId,
+    refreshTrigger,
   ]);

Alternatively, require callers to memoize transactionsData with useMemo if the full object is needed.

🧹 Nitpick comments (2)
src/hooks/useTokenPnL.ts (2)

74-81: Remove commented debug code.

This commented-out console.log block should be removed to keep the codebase clean.

Apply this diff:

-    // Debug log (can remove later)
-    /*
-    console.log(`[useTokenPnL] Hook called for ${token?.symbol}`, {
-      hasTransactionsData: !!transactionsData,
-      txCount: transactionsData?.data?.transactions?.length,
-      walletAddress,
-      chainId
-    });
-    */
-

68-68: Consider simplifying return statements.

Explicit return undefined can be simplified to just return; for cleaner code. This pattern appears at lines 68, 93, 105, 231, and 239.

Also applies to: 93-93, 105-105, 231-231, 239-239

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a1824b and 2a82c8f.

📒 Files selected for processing (2)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx (1 hunks)
  • src/hooks/useTokenPnL.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/hooks/useTokenPnL.ts
📚 Learning: 2025-11-04T14:34:00.373Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 445
File: src/containers/Authorized.tsx:63-77
Timestamp: 2025-11-04T14:34:00.373Z
Learning: In src/containers/Authorized.tsx, the tempKit useEffect intentionally excludes chainId from its dependency array because the kit is used by useWalletModeVerification to check wallet mode across all supported chains, regardless of the currently selected chainId. The chainId parameter is only used for initial kit configuration and does not affect the multi-chain verification results.

Applied to files:

  • src/hooks/useTokenPnL.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (2)
src/hooks/useTokenPnL.ts (2)

1-27: LGTM! Clean type definitions.

The imports and interface definitions are well-structured and provide clear contracts for the hook's inputs and outputs.


29-52: LGTM! Solid refetch mechanism.

The use of useCallback for the refetch function and the separate refreshTrigger state provides a clean manual refresh capability.

- Move 'Hide <$0.5' toggle from PortfolioTokenList to Search.tsx header
- Toggle appears next to 'My Holdings' text in Sell mode
- Filters tokens with balance < $0.5 when enabled
- Fix test failures by adding complete RTK Query API mocks
- Add TestWrapper to Sell.test.tsx, HomeScreen.test.tsx, AppWrapper.test.tsx
- Add mocks for useTokenPnL, pillarXApiWalletTransactions, useRelayBuy, useRemoteConfig
- Update test snapshots for all modified components

Test Results:
- 946/951 tests passing (99.5% pass rate)
- Fixed 68 test failures across 4 test files
- UI manually verified and working correctly
@github-actions github-actions bot temporarily deployed to Preview (feature/pulse-search-ui-mobile-refinements) November 27, 2025 09:01 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/apps/pulse/components/Search/PortfolioTokenList.tsx (3)

5-9: Clean up unused PnL-related imports

PnLMetrics, WalletTransactionsMobulaResponse, fetchRelayRequestByHash, RelayRequest, and the pnl utility imports are not used in this component. Removing them will avoid lint noise and keep the bundle leaner.

-import {
-  PortfolioData,
-  PnLMetrics,
-  WalletTransactionsMobulaResponse,
-} from '../../../../types/api';
+import { PortfolioData } from '../../../../types/api';
@@
-import {
-  fetchRelayRequestByHash,
-  RelayRequest,
-} from '../../../../services/relayApi';
-
-// utils
-import {
-  reconstructTrades,
-  calculatePnL as calculatePnLMetrics,
-  calculatePnLFromRelay,
-} from '../../../../utils/pnl';
+// (relayApi and pnl utilities can be re-imported here when needed)

Also applies to: 33-43


76-81: Remove debug console.log from production component

The console.log in PortfolioTokenList will fire on every render and spam logs in production. It should either be removed or gated behind an explicit debug flag/env check.

-  console.log('[PortfolioTokenList] Component rendering!', {
-    propsKeys: Object.keys(props),
-    hasPortfolioData: !!props.walletPortfolioData,
-  });

92-103: Consolidate sorting and clarify PnL sort behavior

Right now:

  • getFilteredPortfolioTokens sorts tokens using sortConfig, and
  • sortedTokens immediately re-sorts portfolioTokens using sortConfig again, while
  • The 'pnl' branch in sortedTokens sets valA/valB to 0, so PnL sorting is effectively a no-op even though the header and indicator are clickable.

This double‑sorting adds complexity with no benefit, and the PnL column currently suggests a working sort that doesn’t actually reorder rows.

Consider:

  • Removing the sort from getFilteredPortfolioTokens so it only filters, and
  • Keeping all sorting logic in sortedTokens (or disabling the PnL header click/indicator until real PnL sorting is wired).
   const getFilteredPortfolioTokens = () => {
@@
-    // Apply sorting
-    tokens = tokens.sort((a: Token, b: Token) => {
-      let valA = 0;
-      let valB = 0;
-
-      switch (sortConfig.key) {
-        case 'price':
-          return a.symbol.localeCompare(b.symbol);
-        case 'balance':
-          valA = (a.price || 0) * (a.balance || 0);
-          valB = (b.price || 0) * (b.balance || 0);
-          break;
-        case 'pnl':
-          valA = (a.price || 0) * (a.balance || 0);
-          valB = (b.price || 0) * (b.balance || 0);
-          break;
-      }
-      return sortConfig.direction === 'asc' ? valA - valB : valB - valA;
-    });
+    // Leave sorting to `sortedTokens` so filtering is isolated here.

And if you don’t plan to support PnL sort yet, you could also temporarily disable the header click:

-          <div
-            className="flex items-center justify-end flex-[1.8] cursor-pointer gap-1.5"
-            onClick={() => handleSort('pnl')}
+          <div
+            className="flex items-center justify-end flex-[1.8] gap-1.5"
           >

Also applies to: 117-158, 174-203, 305-311

🧹 Nitpick comments (2)
src/apps/pulse/components/Search/tests/PortfolioTokenList.test.tsx (1)

16-35: Mocks and new hideSmallBalances prop usage look sound

The added mocks for useTransactionKit and useGetWalletTransactionsQuery plus the new hideSmallBalances prop in defaultProps integrate cleanly with the updated PortfolioTokenList API. Explicitly disabling the filter in the “null price” test (hideSmallBalances={false}) is a good way to cover that edge case.

If you want fuller coverage later, you could add a small test that verifies a token just under the $0.5 threshold disappears when hideSmallBalances is true, but this isn’t blocking.

Also applies to: 98-105, 321-362

src/apps/pulse/components/Search/Search.tsx (1)

451-460: Pass walletPortfolioFetching through to PortfolioTokenList.isFetching

PortfolioTokenList now supports an isFetching prop and renders SkeletonTokenRows plus opacity changes when it’s true, but Search doesn’t currently pass walletPortfolioFetching down, so that refresh skeleton path will never activate.

Consider wiring it like this so auto-refresh and manual refresh show the intended skeleton state:

        {searchType === SearchType.MyHoldings && (
           <div className="flex flex-col">
             <PortfolioTokenList
               walletPortfolioData={walletPortfolioData}
               handleTokenSelect={handleTokenSelect}
               isLoading={walletPortfolioLoading}
               isError={walletPortfolioError}
               searchText={searchText}
-              hideSmallBalances={hideSmallBalances}
+              hideSmallBalances={hideSmallBalances}
+              isFetching={walletPortfolioFetching}
             />
           </div>
        )}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a82c8f and b5deddc.

⛔ Files ignored due to path filters (5)
  • src/apps/pulse/components/App/tests/__snapshots__/AppWrapper.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/App/tests/__snapshots__/HomeScreen.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Buy/tests/__snapshots__/Buy.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Search/tests/__snapshots__/PortfolioTokenList.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pulse/components/Sell/tests/__snapshots__/Sell.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • src/apps/pulse/components/App/tests/AppWrapper.test.tsx (1 hunks)
  • src/apps/pulse/components/App/tests/HomeScreen.test.tsx (1 hunks)
  • src/apps/pulse/components/Buy/tests/Buy.test.tsx (2 hunks)
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx (5 hunks)
  • src/apps/pulse/components/Search/Search.tsx (4 hunks)
  • src/apps/pulse/components/Search/tests/PortfolioTokenList.test.tsx (3 hunks)
  • src/apps/pulse/components/Sell/tests/Sell.test.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/pulse/components/Sell/tests/Sell.test.tsx
  • src/apps/pulse/components/Buy/tests/Buy.test.tsx
  • src/apps/pulse/components/App/tests/AppWrapper.test.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Search/tests/PortfolioTokenList.test.tsx
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Search/Search.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Search/tests/PortfolioTokenList.test.tsx
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/apps/pulse/components/Search/Search.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Search/Search.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Search/tests/PortfolioTokenList.test.tsx
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required text field. The TileTitleProps interface aligns with the TokensMarketData.title type in api.ts which also has optional fields.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
🧬 Code graph analysis (2)
src/apps/pulse/components/Sell/tests/Sell.test.tsx (1)
src/test-utils/testUtils.tsx (1)
  • TestWrapper (116-134)
src/apps/pulse/components/Search/PortfolioTokenList.tsx (5)
src/types/api.ts (1)
  • PortfolioData (739-748)
src/services/tokensData.ts (2)
  • Token (19-29)
  • chainNameToChainIdTokensData (234-255)
src/services/pillarXApiWalletPortfolio.ts (1)
  • convertPortfolioAPIResponseToToken (27-61)
src/utils/blockchain.ts (1)
  • getLogoForChainId (165-199)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (6)
src/apps/pulse/components/Buy/tests/Buy.test.tsx (2)

27-107: Mock wiring for PnL/Relay/remote-config looks consistent

The added mocks for useTokenPnL, wallet transactions, Relay hooks, search tokens, and useRemoteConfig mirror patterns used elsewhere and should keep Buy tests isolated from network/state. Just keep an eye on these mock shapes if the real hooks’ signatures change so tests don’t silently diverge from production behavior.

If you later change any of these hooks’ return signatures, update the mocks here in lockstep to avoid subtle test breakages.


474-481: More robust “missing portfolio data” assertion

Switching to getAllByText('$0.00') with a count check is a good way to tolerate multiple $0.00 renderings while still asserting the empty-wallet state.

src/apps/pulse/components/Sell/tests/Sell.test.tsx (1)

16-50: Sell tests correctly wired to new PnL and wallet-transactions plumbing

Wrapping <Sell /> in TestWrapper and adding mocks for useTokenPnL, useGetWalletTransactionsQuery, and useRemoteConfig keeps these tests aligned with the new infrastructure without pulling in real network state. The existing interaction and state tests remain valid with these additions.

Also applies to: 129-135

src/apps/pulse/components/App/tests/HomeScreen.test.tsx (1)

32-73: HomeScreen tests now isolated from PnL/Relay and wallet-transactions side effects

The new mocks for useTokenPnL, useGetWalletTransactionsQuery, Relay hooks, and useRemoteConfig give you deterministic HomeScreen renders while the real PnL/Relay logic evolves. This is a solid approach; just ensure these mocked return shapes stay in sync with the underlying hooks.

If you later extend useTokenPnL or the wallet-transactions hook (e.g., extra flags or fields), mirror those in these mocks so assertions still cover realistic states.

src/apps/pulse/components/App/tests/AppWrapper.test.tsx (1)

36-77: AppWrapper tests aligned with new PnL and Relay wiring

Mocking useTokenPnL, useGetWalletTransactionsQuery, Relay hooks, and useRemoteConfig here mirrors the approach in other Pulse tests and cleanly decouples routing/UI assertions from data-layer changes. This should make future PnL/Relay tweaks less brittle for these tests.

src/apps/pulse/components/Search/Search.tsx (1)

85-88: Hide-small-balances state and toggle are wired correctly for MyHoldings

The hideSmallBalances state, sell-only “Hide <$0.5” toggle, and propagation of that flag into PortfolioTokenList for SearchType.MyHoldings are coherent and should give a clear UX for trimming tiny positions while selling.

Also applies to: 373-388, 451-460

- Move PnL Stats in Buy.tsx to appear after buy button (matching Sell layout)
- Remove debug console.log from Sell.tsx
- Update @mui/joy to 5.0.0-beta.48 to resolve @mui/base deprecation
- Add eslint override to force v8.57.0 across all dependencies
- Fix Search.test.tsx Redux context errors by removing Provider and using proper mocks
- Update test snapshots for PortfolioTokenList
@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) November 28, 2025 07:42 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/apps/pulse/components/Sell/Sell.tsx (1)

103-122: Type casting bypasses type safety; consider defining proper types.

The as any casts suppress TypeScript's type checking. While this works, it reduces maintainability and can hide type mismatches.

Additionally, there's an inconsistency: Sell.tsx requires portfolioToken to be present (line 104), while Buy.tsx (lines 177-192) does not use portfolioToken and thus passes potentially undefined balance and price values. This could lead to incorrect PnL calculations in the Buy component.

Consider defining a proper interface for the useTokenPnL props:

-  const { pnl, isLoading: isPnLLoading } = useTokenPnL(
-    token && accountAddress && portfolioToken
-      ? ({
-        token: {
-          ...token,
-          contract: token.address || '',
-          id: token.symbol,
-          blockchain: 'ethereum',
-          balance: portfolioToken.balance || 0,
-          price: portfolioToken.price || 0,
-          decimals: token.decimals || 18,
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        } as any,
-        transactionsData,
-        walletAddress: accountAddress,
-        chainId: token.chainId,
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      } as any)
-      : null
-  );
+  const { pnl, isLoading: isPnLLoading } = useTokenPnL(
+    token && accountAddress && portfolioToken
+      ? {
+          token: {
+            contract: token.address || '',
+            symbol: token.symbol,
+            decimals: token.decimals || 18,
+            balance: portfolioToken.balance || 0,
+            price: portfolioToken.price || 0,
+          },
+          transactionsData,
+          walletAddress: accountAddress,
+          chainId: token.chainId,
+        }
+      : null
+  );

This requires updating the UseTokenPnLProps interface in useTokenPnL.ts to accept this structure.

src/apps/pulse/components/Buy/Buy.tsx (1)

810-813: Consider a type-safe union for expressIntentResponse prop.

The as any cast is used to pass buyOffer when using Relay Buy. While this works, a cleaner approach would be to update the BuyButton prop type to accept a union type:

expressIntentResponse: ExpressIntentResponse | BuyOffer | null

This would maintain type safety while supporting both flows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5deddc and 16d409a.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • src/apps/pulse/components/Search/tests/__snapshots__/PortfolioTokenList.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • package.json (2 hunks)
  • src/apps/pulse/components/Buy/Buy.tsx (7 hunks)
  • src/apps/pulse/components/Search/tests/Search.test.tsx (15 hunks)
  • src/apps/pulse/components/Sell/Sell.tsx (10 hunks)
  • src/apps/pulse/components/Sell/tests/Sell.test.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/apps/pulse/components/Sell/tests/Sell.test.tsx
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Search/tests/Search.test.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Search/tests/Search.test.tsx
📚 Learning: 2025-11-04T14:34:00.373Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 445
File: src/containers/Authorized.tsx:63-77
Timestamp: 2025-11-04T14:34:00.373Z
Learning: In src/containers/Authorized.tsx, the tempKit useEffect intentionally excludes chainId from its dependency array because the kit is used by useWalletModeVerification to check wallet mode across all supported chains, regardless of the currently selected chainId. The chainId parameter is only used for initial kit configuration and does not affect the multi-chain verification results.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
🧬 Code graph analysis (3)
src/apps/pulse/components/Buy/Buy.tsx (2)
src/apps/pulse/utils/blockchain.ts (1)
  • isNativeToken (17-18)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (114-205)
src/apps/pulse/components/Sell/Sell.tsx (4)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (29-266)
src/apps/pulse/utils/blockchain.ts (1)
  • isNativeToken (17-18)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (114-205)
src/apps/pulse/components/Search/tests/Search.test.tsx (2)
src/theme/index.ts (1)
  • defaultTheme (83-163)
src/apps/pulse/components/Search/Search.tsx (1)
  • Search (64-475)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (12)
src/apps/pulse/components/Search/tests/Search.test.tsx (3)

134-140: Good refactor for test consistency.

The renderWithProviders helper is a clean pattern that reduces boilerplate and ensures all tests have consistent theming and routing context.


148-159: LGTM: Snapshot test now includes theme context.

Adding ThemeProvider to the snapshot test ensures that theme-dependent styling is captured in the snapshot, making it more comprehensive.


162-344: Consistent test scaffolding improvements.

All test cases have been consistently updated to use renderWithProviders, ensuring uniform theming and routing context across the test suite. This improves maintainability and consistency.

src/apps/pulse/components/Sell/Sell.tsx (3)

92-100: LGTM!

The portfolioToken lookup correctly matches by both contract address (case-insensitive) and chain ID, with proper null guards.


84-89: LGTM!

The transactions query correctly skips when accountAddress is unavailable, preventing unnecessary API calls.


617-628: LGTM!

The conditional rendering correctly shows the skeleton during loading and only displays PnL data when there's actual trading history. The short-circuit evaluation properly guards against null pnl access.

src/apps/pulse/components/Buy/Buy.tsx (3)

169-174: LGTM!

The transactions query pattern is consistent with Sell.tsx and correctly handles the skip condition.


831-842: PnL rendering logic is correct but will rarely display due to upstream issue.

The conditional rendering pattern is correct and consistent with Sell.tsx. However, due to the missing balance and price properties flagged above (lines 176-192), the pnl object will typically be null (hook returns early with "Small Balance" status), so this section will only flash during loading and then disappear.

Once the upstream issue is fixed, this section will work as intended.


779-782: Minor formatting adjustments look fine.

The className template literal formatting is consistent with the codebase style.

package.json (3)

168-168: viem override is consistent with dependency version.

The viem override at line 168 correctly pins version 2.37.1, which matches the dependency at line 87. No action needed.


121-121: <function_calls>

#!/bin/bash

Read package.json lines around 121 and 169 to verify eslint version

echo "=== package.json lines 120-125 ==="
sed -n '120,125p' package.json

echo ""
echo "=== package.json lines 168-172 ==="
sed -n '168,172p' package.json

echo ""
echo "=== Looking for eslint config files ==="
find . -maxdepth 2 -type f ( -name '.eslintrc*' -o -name 'eslint.config.*' ) 2>/dev/null | head -20

echo ""
echo "=== Checking if eslint config is in package.json ==="
grep -A 20 '"eslintConfig"' package.json | head -30


</function_calls>

#!/bin/bash
# Examine a few key new PnL files to understand their content
echo "=== Examining src/utils/pnl.ts ==="
wc -l src/utils/pnl.ts

echo ""
echo "=== Examining src/hooks/useTokenPnL.ts ==="
wc -l src/hooks/useTokenPnL.ts

echo ""
echo "=== Examining src/services/relayApi.ts ==="
wc -l src/services/relayApi.ts

echo ""
echo "=== Checking for obvious syntax issues in pnl.ts ==="
head -50 src/utils/pnl.ts
</parameter>
</invoke>
</function_calls>

---

`30-30`: **No testing required for @mui/joy version bump—new PnL components don't use it.**

The new PnL components (PnLStats, TokenPnLCell) are built with Tailwind CSS, not @mui/joy, so the version bump from 5.0.0-beta.22 to 5.0.0-beta.48 does not affect them. Additionally, the changelog between these beta versions contains only bug fixes and documentation updates with no breaking changes. The existing @mui/joy usage elsewhere in the codebase (AssetSelect, fear-and-greed app) remains compatible with beta.48.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) November 28, 2025 13:31 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/hooks/useTokenPnL.ts (1)

269-279: Auto-refresh interval still depends on unstable props object.

The interval effect depends on the entire props object reference, which changes on every parent render. This causes the interval to be cleared and recreated unnecessarily.

Apply the fix suggested in the previous review:

   useEffect(() => {
     if (!props) return undefined;

     const interval = setInterval(() => {
       setResult((prev) => ({ ...prev, isLoading: true }));
       setRefreshTrigger((prev) => prev + 1);
     }, 30000); // 30 seconds

     return () => clearInterval(interval);
-  }, [props]);
+  }, [!!props]);
src/apps/pulse/components/Search/tests/Search.test.tsx (1)

7-8: Remove unused imports.

Provider and configureStore are imported but never used in this test file.

-import { Provider } from 'react-redux';
-import { configureStore } from '@reduxjs/toolkit';
 import { ThemeProvider } from 'styled-components';
src/apps/pulse/components/Search/TokenPnLCell.tsx (1)

51-63: Negative PnL values don't display explicit minus sign.

For negative unrealisedPnLUSDC, the cell renders $5.00 (in red) without a minus sign, relying solely on color to indicate loss. This is inconsistent with typical financial displays and could be confusing.

As noted in a previous review, compute signs explicitly:

+  const usdSign =
+    pnl.unrealisedPnLUSDC > 0 ? '+' : pnl.unrealisedPnLUSDC < 0 ? '-' : '';
+  const pctSign =
+    pnl.unrealisedPnLPct > 0 ? '+' : pnl.unrealisedPnLPct < 0 ? '-' : '';
+
   return (
     <div className="flex flex-col items-end">
       <p
         className={`text-[13px] font-normal tracking-[-0.26px] text-right ${
           pnl.unrealisedPnLUSDC >= 0 ? 'text-[#4ADE80]' : 'text-[#EF4444]'
         }`}
       >
-        {pnl.unrealisedPnLUSDC >= 0 ? '+' : ''}$
+        {usdSign}$
         {formatExponentialSmallNumber(
           limitDigitsNumber(Math.abs(pnl.unrealisedPnLUSDC))
         )}
       </p>
       <p
         className={`text-xs font-normal tracking-[-0.24px] text-right ${
           pnl.unrealisedPnLPct >= 0 ? 'text-[#4ADE80]' : 'text-[#EF4444]'
         }`}
       >
-        {pnl.unrealisedPnLPct >= 0 ? '+' : ''}
-        {pnl.unrealisedPnLPct.toFixed(2)}%
+        {pctSign}
+        {Math.abs(pnl.unrealisedPnLPct).toFixed(2)}%
       </p>
     </div>
   );
src/apps/pulse/components/Search/PortfolioTokenList.tsx (1)

130-193: Consolidate sorting and clarify pnl sort behavior

Right now sorting is applied twice and pnl sorting is misleading:

  • getFilteredPortfolioTokens sorts tokens (lines 145-178) based on sortConfig, then
  • sortedTokens re-sorts portfolioTokens again (lines 198-227), with the 'pnl' case setting valA/valB to 0, so that comparator is effectively a no‑op.
  • For sortConfig.key === 'pnl', the final ordering ends up coming from the first sort (by balance USD), while the UI header and code comments suggest either PnL-based sorting or disabled sorting.

This is both redundant work and confusing behavior for users.

I’d recommend:

  • Remove sorting from getFilteredPortfolioTokens so it only handles filtering/search, and
  • Keep all sorting centralized in sortedTokens (or vice versa), and
  • Either:
    • Implement a real PnL sort once metrics are available at this level, or
    • Temporarily disable sorting for the PnL column (no click handler / indicator) or make it explicitly sort by balance USD as a documented proxy.

Example of removing the first sort block:

-    // Apply sorting
-    tokens = tokens.sort((a: Token, b: Token) => {
-      let valA = 0;
-      let valB = 0;
-
-      switch (sortConfig.key) {
-        case 'price':
-          return a.symbol.localeCompare(b.symbol);
-        case 'balance':
-          valA = (a.price || 0) * (a.balance || 0);
-          valB = (b.price || 0) * (b.balance || 0);
-          break;
-        case 'pnl':
-          valA = (a.price || 0) * (a.balance || 0);
-          valB = (b.price || 0) * (b.balance || 0);
-          break;
-        default:
-          valA = (a.price || 0) * (a.balance || 0);
-          valB = (b.price || 0) * (b.balance || 0);
-          break;
-      }
-      return sortConfig.direction === 'asc' ? valA - valB : valB - valA;
-    });

and then adjust sortedTokens / the PnL header to reflect the actual intended behavior.

Also applies to: 198-227, 329-390

🧹 Nitpick comments (7)
src/hooks/useTokenPnL.ts (3)

42-47: Remove console.log statements before merging.

Debug logging on every render will pollute the console in production. Consider using a debug flag or removing these logs entirely.

-  // eslint-disable-next-line no-console
-  console.log('[useTokenPnL] Hook render', {
-    tokenSymbol: props?.token?.symbol,
-    isLoading: result.isLoading,
-    pnl: result.pnl,
-  });

190-196: Redundant isMounted check after early return.

Line 190 returns early if !isMounted, so the check on line 192 is always true and unnecessary.

     if (!isMounted) return;

-    if (isMounted)
-      setResult((prev) => ({
+    setResult((prev) => ({
         ...prev,
         debug: { ...prev.debug, relayResponseCount: relayRequests.length },
-      }));
+    }));

104-108: Additional console.log statements in async flow.

These debug statements should also be removed for production code.

-      // eslint-disable-next-line no-console
-      console.log('[useTokenPnL] calculatePnL start', {
-        hasTransactions: !!transactions,
-        walletAddress: !!walletAddress,
-      });
-          // eslint-disable-next-line no-console
-          console.log('[useTokenPnL] Calculation complete', pnlMetrics);

Also applies to: 236-237

src/apps/pulse/components/Search/tests/Search.test.tsx (1)

24-26: Unused import: pillarXApiWalletTransactions.

The named import pillarXApiWalletTransactions on line 25 is imported but not used in the test file. It's only defined in the mock.

-// services
-import { pillarXApiWalletTransactions } from '../../../../../services/pillarXApiWalletTransactions';
src/apps/pulse/components/Search/TokenPnLCell.tsx (1)

29-43: Consider deduplicating identical loading and no-data states.

Both isLoading and !pnl conditions render the same JSX. This could be simplified.

-  if (isLoading) {
-    return (
-      <div className="flex flex-col items-end">
-        <p className="text-xs text-white/30">-</p>
-      </div>
-    );
-  }
-
-  if (!pnl) {
+  if (isLoading || !pnl) {
     return (
       <div className="flex flex-col items-end">
         <p className="text-xs text-white/30">-</p>
       </div>
     );
   }
src/apps/pulse/components/Search/PortfolioTokenList.tsx (2)

229-231: Remove stale commented-out PnL summary logic

These comments refer to a removed “Total Portfolio PnL” summary box and even quote a prior user conversation. They don’t add value for future maintainers and can be safely deleted to keep the component clean.

-  // Calculate Total Portfolio PnL (Removed as per new design, but keeping logic if needed later? No, remove to clean up)
-  // The user explicitly said "nope its not here", implying the summary box shouldn't be there.

471-479: Clean up stray PnL-related comments in the price cell

The inline comments about “Skip PnL for small balances” and valueUSD are unrelated to the token price display and look like leftovers from earlier iterations. They can confuse future readers.

Consider removing this block entirely and leaving only the price formatting:

-                      ${/* // Skip PnL for small balances */}
-                      {/* if (valueUSD < 0.01) { */}
-                      {/*   return { ...token, pnl: null }; */}
-                      {/* } */}
-                      {token.price
+                      $
+                      {token.price
                         ? formatExponentialSmallNumber(
                             limitDigitsNumber(token.price)
                           )
                         : '0.00'}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16d409a and 7fbe732.

⛔ Files ignored due to path filters (3)
  • src/apps/pulse/components/Buy/tests/__snapshots__/Buy.test.tsx.snap is excluded by !**/*.snap
  • src/apps/the-exchange/components/CardsSwap/test/__snapshots__/CardSwap.test.tsx.snap is excluded by !**/*.snap
  • src/apps/token-atlas/components/TokenGraphColumn/test/__snapshots__/TokenGraphColumn.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (12)
  • src/apps/pulse/components/Buy/Buy.tsx (8 hunks)
  • src/apps/pulse/components/Buy/tests/Buy.test.tsx (2 hunks)
  • src/apps/pulse/components/PnLStats/PnLStats.tsx (1 hunks)
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx (5 hunks)
  • src/apps/pulse/components/Search/Search.tsx (3 hunks)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx (1 hunks)
  • src/apps/pulse/components/Search/tests/Search.test.tsx (15 hunks)
  • src/apps/pulse/components/Sell/Sell.tsx (10 hunks)
  • src/apps/pulse/components/Sell/tests/Sell.test.tsx (3 hunks)
  • src/apps/token-atlas/components/TokenGraphColumn/TokenGraphColumn.tsx (0 hunks)
  • src/hooks/useTokenPnL.ts (1 hunks)
  • src/services/pillarXApiWalletPortfolio.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/apps/token-atlas/components/TokenGraphColumn/TokenGraphColumn.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/apps/pulse/components/Sell/tests/Sell.test.tsx
  • src/apps/pulse/components/PnLStats/PnLStats.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Search/Search.tsx
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Search/tests/Search.test.tsx
  • src/hooks/useTokenPnL.ts
  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required text field. The TileTitleProps interface aligns with the TokensMarketData.title type in api.ts which also has optional fields.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required title field. The TileTitleProps structure aligns with how it's used in the TokensMarketData type in api.ts.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Search/tests/Search.test.tsx
  • src/apps/pulse/components/Buy/tests/Buy.test.tsx
📚 Learning: 2025-11-04T14:34:00.373Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 445
File: src/containers/Authorized.tsx:63-77
Timestamp: 2025-11-04T14:34:00.373Z
Learning: In src/containers/Authorized.tsx, the tempKit useEffect intentionally excludes chainId from its dependency array because the kit is used by useWalletModeVerification to check wallet mode across all supported chains, regardless of the currently selected chainId. The chainId parameter is only used for initial kit configuration and does not affect the multi-chain verification results.

Applied to files:

  • src/hooks/useTokenPnL.ts
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
🧬 Code graph analysis (5)
src/apps/pulse/components/Search/PortfolioTokenList.tsx (5)
src/types/api.ts (1)
  • PortfolioData (739-748)
src/services/tokensData.ts (2)
  • Token (19-29)
  • chainNameToChainIdTokensData (234-255)
src/services/pillarXApiWalletPortfolio.ts (1)
  • convertPortfolioAPIResponseToToken (27-61)
src/utils/blockchain.ts (1)
  • getLogoForChainId (165-199)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/apps/pulse/components/Search/tests/Search.test.tsx (2)
src/theme/index.ts (1)
  • defaultTheme (83-163)
src/apps/pulse/components/Search/Search.tsx (1)
  • Search (64-484)
src/hooks/useTokenPnL.ts (4)
src/services/tokensData.ts (1)
  • Token (19-29)
src/types/api.ts (2)
  • WalletTransactionsMobulaResponse (792-801)
  • PnLMetrics (815-826)
src/utils/pnl.ts (2)
  • calculatePnL (104-180)
  • calculatePnLFromRelay (366-508)
src/services/relayApi.ts (2)
  • RelayRequest (3-77)
  • fetchRelayRequestByHash (107-130)
src/apps/pulse/components/Sell/Sell.tsx (4)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (29-282)
src/apps/pulse/utils/blockchain.ts (1)
  • isNativeToken (17-18)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (113-200)
src/apps/pulse/components/Search/TokenPnLCell.tsx (4)
src/services/tokensData.ts (1)
  • Token (19-29)
src/types/api.ts (1)
  • WalletTransactionsMobulaResponse (792-801)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (29-282)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (10)
src/services/pillarXApiWalletPortfolio.ts (1)

193-193: Cache configuration looks good.

The 5-minute cache duration is reasonable for portfolio data and follows RTK Query best practices. Since keepUnusedDataFor only applies after components unmount, it won't interfere with the 30-second auto-refresh mentioned in the PR objectives. When users return to the page, they'll see cached data briefly while fresh data loads in the background.

Please confirm that a 5-minute cache window aligns with your product requirements for PnL data freshness, especially for scenarios where users navigate away and return to portfolio views.

src/apps/pulse/components/Buy/tests/Buy.test.tsx (4)

27-33: Good mock setup for the new useTokenPnL hook.

The mock provides sensible defaults with pnl: null, isLoading: false, and a mock refetch function. This aligns well with the hook's return type.


58-69: Mock for wallet transactions API is well-structured.

The mock includes both the RTK Query hook and the API slice scaffolding (reducerPath, reducer, middleware), which is necessary to avoid Redux configuration errors during tests.


95-108: Relay Buy and remote config mocks added correctly.

These mocks support the new Relay Buy integration tests added at lines 926-1026.


481-481: Less brittle assertion for missing wallet data.

Using getAllByText('$0.00').length > 0 instead of an exact count is more resilient to UI changes while still verifying the expected behavior.

src/apps/pulse/components/Search/tests/Search.test.tsx (2)

135-141: Well-structured test helper with theme context.

The renderWithProviders helper properly wraps components with ThemeProvider and MemoryRouter, ensuring consistent test context across all tests.


32-62: Comprehensive mocks for new PnL and transaction dependencies.

The mocks for useTokenPnL, pillarXApiWalletTransactions, and useTransactionKit are well-structured and provide appropriate default values for testing.

src/apps/pulse/components/Sell/Sell.tsx (3)

92-100: Good use of useMemo for portfolio token lookup.

The memoization correctly depends on token and portfolioTokens, preventing unnecessary recalculations when other state changes.


625-635: PnL stats display logic is well-implemented.

The conditional rendering correctly shows the PnLStats component only when there's meaningful data (totalBoughtUSDC > 0 || totalSoldUSDC > 0) or during loading, preventing empty UI states.


125-129: PnL refetch integration looks correct.

The effect properly triggers refetchPnL when isRefreshing changes, allowing parent components to coordinate refresh cycles.

- Buy.tsx: Use portfolioToken for PnL calculation to match Sell.tsx logic
- PnLStats.tsx: Fix negative value formatting for USD and percentage
- CardSwap.test.tsx: Update snapshot to reflect removed button
- Buy.test.tsx & Sell.test.tsx: Update snapshots for PnL formatting changes
- Remove extra space in refetch definition
- Remove unused transactionsData variable
- Suppress exhaustive-deps warning for intentional omission
@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) November 28, 2025 14:01 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/hooks/useTokenPnL.ts (3)

270-279: Auto-refresh interval restarts unnecessarily on props reference changes.

The effect depends on the entire props object (line 279), causing the interval to be cleared and recreated whenever the props reference changes—even if the content is identical. This could lead to unpredictable refresh timing and missed intervals.

Depend only on whether props exists:

  useEffect(() => {
    if (!props) return undefined;

    const interval = setInterval(() => {
      setResult((prev) => ({ ...prev, isLoading: true }));
      setRefreshTrigger((prev) => prev + 1);
    }, 30000); // 30 seconds

    return () => clearInterval(interval);
-  }, [props]);
+  }, [!!props]);

Alternatively, extract the specific values needed from props and depend on those, or omit the dependency entirely if you want the interval to run once for the hook's lifetime after props becomes truthy.


49-57: Critical: Missing destructure of transactionsData from props.

Line 55 attempts to access props?.transactionsData?.data?.transactions, but transactionsData is never destructured from props. This will cause the hook to always evaluate transactions as undefined, preventing PnL calculation.

Add the missing destructure:

  const token = props?.token;
  const tokenContract = props?.token?.contract;
  const tokenSymbol = props?.token?.symbol;
  const tokenDecimals = props?.token?.decimals;
  const tokenBalance = props?.token?.balance;
  const tokenPrice = props?.token?.price;
+ const transactionsData = props?.transactionsData;
  const transactions = props?.transactionsData?.data?.transactions;
  const walletAddress = props?.walletAddress;
  const chainId = props?.chainId;

208-214: Replace non-null assertion with proper validation.

The non-null assertion chainId! at line 212 assumes chainId is always defined, but this is not guaranteed by the preceding validation checks. If props are null or chainId is undefined, this could cause runtime errors.

Add explicit chainId validation:

      // Calculate PnL from Relay data
+     if (!chainId) {
+       if (isMounted)
+         setResult((prev) => ({
+           ...prev,
+           isLoading: false,
+           debug: { ...prev.debug, status: 'No ChainId' },
+         }));
+       return;
+     }
+
      const trades = calculatePnLFromRelay(relayRequests, {
        address: tokenContract || '',
        symbol: tokenSymbol || '',
        decimals: tokenDecimals || 18,
-       chainId: chainId!,
+       chainId,
        price: tokenPrice,
      });
🧹 Nitpick comments (3)
src/hooks/useTokenPnL.ts (1)

42-47: Remove or guard console.log statements in production code.

Console logging on every render can impact performance and potentially leak sensitive debugging information in production builds.

Consider wrapping these in a development-only check or removing them:

-  // eslint-disable-next-line no-console
-  console.log('[useTokenPnL] Hook render', {
-    tokenSymbol: props?.token?.symbol,
-    isLoading: result.isLoading,
-    pnl: result.pnl,
-  });
+  if (process.env.NODE_ENV === 'development') {
+    // eslint-disable-next-line no-console
+    console.log('[useTokenPnL] Hook render', {
+      tokenSymbol: props?.token?.symbol,
+      isLoading: result.isLoading,
+      pnl: result.pnl,
+    });
+  }

Apply the same pattern to the console.log at line 236.

src/apps/pulse/components/Buy/Buy.tsx (2)

187-207: Type casting to any bypasses type safety for PnL hook.

The token object is cast to any (lines 200, 205) to satisfy useTokenPnL's type requirements, which indicates a type mismatch between SelectedToken and the expected Token type. This bypasses TypeScript's type checking and could hide potential runtime errors.

Consider either:

  1. Creating a proper type adapter function that maps SelectedToken to the expected Token type, or
  2. Updating the Token type in useTokenPnL to accept the properties being passed

Example adapter approach:

const adaptTokenForPnL = (
  selectedToken: SelectedToken,
  portfolioToken: PortfolioToken
): Token => ({
  ...selectedToken,
  contract: selectedToken.address || '',
  id: selectedToken.symbol,
  blockchain: 'ethereum',
  balance: portfolioToken.balance || 0,
  price: portfolioToken.price || 0,
  decimals: selectedToken.decimals || 18,
});

Then use it:

const { pnl, isLoading: isPnLLoading, refetch: refetchPnL } = useTokenPnL(
  token && accountAddress && portfolioToken
    ? {
        token: adaptTokenForPnL(token, portfolioToken),
        transactionsData,
        walletAddress: accountAddress,
        chainId: token.chainId,
      }
    : null
);

830-833: Type casting buyOffer to any bypasses type safety.

The buyOffer is cast to any to pass it as expressIntentResponse to BuyButton, indicating that BuyOffer and ExpressIntentResponse are incompatible types. This bypasses type checking and could lead to runtime errors if BuyButton accesses properties that don't exist on BuyOffer.

Consider updating BuyButton to accept a union type:

// In BuyButton component
interface BuyButtonProps {
  // ... other props
  expressIntentResponse: ExpressIntentResponse | BuyOffer | null;
  // ... other props
}

Then remove the type casting:

  expressIntentResponse={
-   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-   USE_RELAY_BUY ? (buyOffer as any) : expressIntentResponse
+   USE_RELAY_BUY ? buyOffer : expressIntentResponse
  }

This preserves type safety while handling both Relay and Intent SDK flows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fbe732 and 87dcaba.

📒 Files selected for processing (3)
  • src/apps/pulse/components/Buy/Buy.tsx (9 hunks)
  • src/apps/pulse/components/PnLStats/PnLStats.tsx (1 hunks)
  • src/hooks/useTokenPnL.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/apps/pulse/components/PnLStats/PnLStats.tsx
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/hooks/useTokenPnL.ts
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
📚 Learning: 2025-11-04T14:34:00.373Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 445
File: src/containers/Authorized.tsx:63-77
Timestamp: 2025-11-04T14:34:00.373Z
Learning: In src/containers/Authorized.tsx, the tempKit useEffect intentionally excludes chainId from its dependency array because the kit is used by useWalletModeVerification to check wallet mode across all supported chains, regardless of the currently selected chainId. The chainId parameter is only used for initial kit configuration and does not affect the multi-chain verification results.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/hooks/useTokenPnL.ts
📚 Learning: 2025-05-28T14:30:02.702Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 320
File: src/components/BottomMenuModal/HistoryModal/TransactionInfo.tsx:177-182
Timestamp: 2025-05-28T14:30:02.702Z
Learning: In the transaction history tracking feature, chain ID validation is performed earlier in the flow before the values reach the TransactionInfo component, so additional validation at the display level is not needed.

Applied to files:

  • src/hooks/useTokenPnL.ts
🧬 Code graph analysis (1)
src/apps/pulse/components/Buy/Buy.tsx (3)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/apps/pulse/utils/blockchain.ts (1)
  • isNativeToken (17-18)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (113-202)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug

- Buy.tsx: Fix runtime error by destructuring transactionsData
- PnLStats.tsx: Fix nested ternary lint error
- Search.test.tsx: Remove unused imports
- useTokenPnL.ts: Fix exhaustive-deps warning by adding token dependency
- Buy.tsx: Fix formatting issues using prettier
@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) November 28, 2025 19:01 Inactive
@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) December 1, 2025 16:46 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/utils/pnl.ts (3)

36-71: Normalize stablecoin symbol casing to avoid missing USDC trades

The reconstruction logic is solid (grouping by tx_hash, netting token/USDC flows, skipping multi‑token/fee rows), but the stablecoin checks are case‑sensitive:

const { symbol } = tx.asset;
if (symbol === 'USDC' || symbol === 'USDT' || symbol === 'DAI') {
  ...
  if (symbol === 'USDC') {

If Mobula ever returns usdc / Usdc, those trades will be ignored for PnL.

Consider normalizing once and using the normalized value only for comparisons, while preserving the original symbol for display:

-      const { symbol } = tx.asset;
+      const rawSymbol = tx.asset.symbol;
+      const symbolUpper = rawSymbol?.toUpperCase?.() ?? rawSymbol;

-      if (symbol === 'USDC' || symbol === 'USDT' || symbol === 'DAI') {
+      if (symbolUpper === 'USDC' || symbolUpper === 'USDT' || symbolUpper === 'DAI') {
         // ...
-        if (symbol === 'USDC') {
+        if (symbolUpper === 'USDC') {
           if (isInbound) usdcChange += amount;
           if (isOutbound) usdcChange -= amount;
         }
-      } else if (tokenSymbol && tokenSymbol !== symbol) {
+      } else if (tokenSymbol && tokenSymbol !== rawSymbol) {
         tokenSymbol = 'INVALID';
       } else {
-        tokenSymbol = symbol;
+        tokenSymbol = rawSymbol;

116-133: Handle oversells and prevent negative inventory in calculatePnL

The SELL branch assumes you never sell more than your current inventory:

if (trade.side === 'BUY') {
  totalTokens += trade.amountToken;
  totalCostUSDC += trade.amountQuoteUSDC;
} else {
  // SELL
  if (totalTokens === 0) return;

  const wac = totalCostUSDC / totalTokens;
  const costBasis = trade.amountToken * wac;

  totalTokens -= trade.amountToken;
  totalCostUSDC -= costBasis;
  ...
}
...
if (totalTokens < 0) totalTokens = 0;
if (totalCostUSDC < 0) totalCostUSDC = 0;

With incomplete history or bad data, a SELL can exceed totalTokens, driving totalTokens and totalCostUSDC negative, and realised PnL/cost‑basis become inconsistent; the end‑loop clamp hides the negative state but doesn’t fix the earlier math.

At minimum, you should:

  • Treat non‑positive inventory as a hard guard, and
  • Clamp the sell amount to available inventory so it never goes negative.

For example:

    } else {
      // SELL
-      if (totalTokens === 0) return; // Selling without inventory (shouldn't happen in perfect history)
-
-      const wac = totalCostUSDC / totalTokens;
-      const costBasis = trade.amountToken * wac;
-
-      totalTokens -= trade.amountToken;
-      totalCostUSDC -= costBasis;
-
-      totalCostBasisSold += costBasis;
-      realisedPnLUSDC += trade.amountQuoteUSDC - costBasis;
+      if (totalTokens <= 0) return; // Guard against zero/negative inventory
+
+      const wac = totalCostUSDC / totalTokens;
+      const sellAmount = Math.min(trade.amountToken, totalTokens);
+      const costBasis = sellAmount * wac;
+
+      totalTokens -= sellAmount;
+      totalCostUSDC -= costBasis;
+
+      totalCostBasisSold += costBasis;
+
+      // Scale quote amount if we clamped sellAmount
+      const quoteProportion =
+        trade.amountToken > 0 ? sellAmount / trade.amountToken : 1;
+      realisedPnLUSDC += trade.amountQuoteUSDC * quoteProportion - costBasis;

Also applies to: 135-138


196-222: Unify USDC address list and include outTxs when scanning Relay state changes

Two related issues here:

  1. Duplicated and inconsistent USDC address lists
  • getRelayValidatedTrades defines USDC_ADDRESSES with 4 chains (Base, Ethereum, Polygon, BSC) on lines 252‑258.
  • calculatePnLFromRelay defines another USDC_ADDRESSES with 6 chains (adds Arbitrum and Optimism) on lines 387‑395.

This can cause getRelayValidatedTrades to miss USDC flows on Arbitrum/Optimism and is error‑prone.

  1. USDC state changes only read from inTxs, ignoring outTxs

In getRelayValidatedTrades, you only inspect relayReq.data.inTxs:

if (relayReq.data?.inTxs) {
  relayReq.data.inTxs.forEach((inTx) => {
    ...
  });
}

For many SELL flows, the USDC leg will be recorded in outTxs, so those trades will appear without a USDC amount and fall back to token_price or be dropped, diverging from the more complete calculatePnLFromRelay path (which correctly combines inTxs and outTxs).

You can address both by introducing a shared, normalized constant and using allTxs in both places. For example:

+// Known USDC addresses (lowercased) across supported chains
+const USDC_ADDRESSES = [
+  '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // Base USDC
+  '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // Ethereum USDC
+  '0x3c499c54b84a76ad7e9c93437bfc5ac33e2ddae9', // Polygon USDC
+  '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', // BSC USDC
+  '0xaf88d065e77c8cc2239327c5edb3a432268e5831', // Arbitrum USDC
+  '0x0b2c639c533813f4aa9d7837caf992837bd5787f', // Optimism USDC
+].map((addr) => addr.toLowerCase());

Then in getRelayValidatedTrades:

-      // For USDC amount, extract it from Relay stateChanges
-      usdcAmount = 0;
-      if (relayReq.data?.inTxs) {
-        relayReq.data.inTxs.forEach((inTx) => {
-          if (inTx.stateChanges) {
-            inTx.stateChanges.forEach((stateChange) => {
+      // For USDC amount, extract it from all Relay stateChanges
+      usdcAmount = 0;
+      const allTxs = [
+        ...(relayReq.data?.inTxs || []),
+        ...(relayReq.data?.outTxs || []),
+      ];
+
+      allTxs.forEach((tx) => {
+        if (tx.stateChanges) {
+          tx.stateChanges.forEach((stateChange) => {
             const tokenAddr =
               stateChange.change?.data?.tokenAddress?.toLowerCase();
             const changeAddress = stateChange.address?.toLowerCase();
@@
-                USDC_ADDRESSES.some(
-                  (addr) => addr.toLowerCase() === tokenAddr
-                ) &&
+                USDC_ADDRESSES.includes(tokenAddr) &&
                 changeAddress === userAddress
             ) {
               ...
-            });
-          }
-        });
-      }
+            });
+          }
+        }
+      });

And in calculatePnLFromRelay, reuse the same USDC_ADDRESSES:

-  // Known USDC addresses for matching quote currency
-  const USDC_ADDRESSES = [ ... ];
+  // Reuse shared USDC_ADDRESSES defined at top of file

Also applies to: 252-262, 304-341, 387-395, 450-463

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3994d8 and 65abc32.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • package.json (1 hunks)
  • src/hooks/useTokenPnL.ts (1 hunks)
  • src/utils/pnl.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useTokenPnL.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/utils/pnl.ts
🧬 Code graph analysis (1)
src/utils/pnl.ts (2)
src/types/api.ts (3)
  • MobulaTransactionRow (768-790)
  • ReconstructedTrade (803-813)
  • PnLMetrics (815-826)
src/services/relayApi.ts (2)
  • RelayRequest (3-99)
  • fetchRelayRequestByHash (130-154)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (2)
src/utils/pnl.ts (1)

375-549: Relay‑first PnL reconstruction logic looks robust

The overall structure of calculatePnLFromRelay is solid:

  • Prefer metadata (currencyIn/currencyOut) when available.
  • Fall back to combined inTxs/outTxs state changes with proper decimal normalization.
  • Apply sensible sanity checks on amountUSDC and execPrice, with conservative fallbacks to token.price or dropping suspicious trades.
  • Return sorted ReconstructedTrade[] consistent with the rest of the module.

Once the shared USDC_ADDRESSES constant is factored out (per previous comment), this function reads cleanly and defensively.

package.json (1)

30-30: Test for UI regressions in AssetSelect and Fear-and-Greed components

@mui/joy's codebase uses modern patterns (sx prop, CssVarsProvider) compatible with the beta versions in play. However, beta-to-beta jumps can still introduce styling or behavioral changes. Verify visually that the Tabs/TabList in AssetSelect and Box/Grid/Typography layouts in Fear-and-Greed render consistently.

@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) December 2, 2025 14:56 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (6)
src/hooks/useTokenPnL.ts (1)

219-225: Non-null assertion on chainId is risky.

The chainId! assertion on line 223 assumes chainId is always defined, but props can be null. While there's an early return check for !token || !walletAddress, chainId could still be 0 or undefined in edge cases.

Use a safe fallback instead:

         const trades = calculatePnLFromRelay(relayRequests, {
           address: tokenContract || '',
           symbol: tokenSymbol || '',
           decimals: tokenDecimals || 18,
-          chainId: chainId!,
+          chainId: chainId ?? 0,
           price: tokenPrice,
         });
src/apps/pulse/components/Search/PortfolioTokenList.tsx (1)

90-123: Duplicate sorting logic persists - remove from getFilteredPortfolioTokens.

Sorting is performed both here (lines 91-123) and again in sortedTokens useMemo (lines 147-171). The extensive comments on lines 97-103 and 109-113 indicate uncertainty about this design. Since sortedTokens already handles all sorting cases, this block is redundant and adds cognitive overhead.

Remove the sorting logic from getFilteredPortfolioTokens and let sortedTokens handle it exclusively:

     // Filter small balances if toggle is enabled
     if (hideSmallBalances) {
       tokens = tokens.filter((token) => {
         const balanceUSD = (token.price || 0) * (token.balance || 0);
         return balanceUSD >= 0.5;
       });
     }
 
-    // Apply sorting
-    tokens = tokens.sort((a: Token, b: Token) => {
-      let valA = 0;
-      let valB = 0;
-
-      switch (sortConfig.key) {
-        case 'price':
-          // Sort by Token Name/Price? Usually alphabetical or by price.
-          // Let's assume alphabetical by symbol for "Token" column sorting, or price?
-          // The header says "Token/Price". Let's sort by Symbol for now as it's more common for "Token".
-          // Or maybe USD Value?
-          // Let's stick to the previous default: Balance USD.
-          // But if user clicks "Token/Price", maybe alphabetical?
-          return a.symbol.localeCompare(b.symbol);
-        case 'balance':
-          valA = (a.price || 0) * (a.balance || 0);
-          valB = (b.price || 0) * (b.balance || 0);
-          break;
-        case 'pnl':
-          // We need PnL data for this. We'll handle PnL sorting in the useMemo below or move PnL calc up.
-          // Since PnL calc depends on transactions, and we are inside getFilteredPortfolioTokens which is called before PnL calc...
-          // We should probably move sorting AFTER PnL calc.
-          // For now, let's return tokens sorted by balance as default, and handle re-sorting in the rendered list or move logic.
-          // To avoid complex refactoring, I'll keep basic filtering here and do final sort on `tokensWithPnL`.
-          valA = (a.price || 0) * (a.balance || 0);
-          valB = (b.price || 0) * (b.balance || 0);
-          break;
-        default:
-          valA = (a.price || 0) * (a.balance || 0);
-          valB = (b.price || 0) * (b.balance || 0);
-          break;
-      }
-      return sortConfig.direction === 'asc' ? valA - valB : valB - valA;
-    });
-
     // Apply search filter if searchText is provided

Also update the dependency array to remove sortConfig:

-  }, [walletPortfolioData, hideSmallBalances, sortConfig, searchText]);
+  }, [walletPortfolioData, hideSmallBalances, searchText]);
src/apps/pulse/components/Search/TokenPnLCell.tsx (1)

70-91: Negative PnL values lack explicit minus sign.

For negative unrealisedPnLUSDC, the cell renders the absolute value without a minus sign (e.g., $5.00 instead of -$5.00), relying solely on red text to indicate a loss. This is inconsistent and could be confusing, especially for accessibility.

Apply explicit signs for clarity:

+  const usdSign =
+    pnl.unrealisedPnLUSDC > 0 ? '+' : pnl.unrealisedPnLUSDC < 0 ? '-' : '';
+  const pctSign =
+    pnl.unrealisedPnLPct > 0 ? '+' : pnl.unrealisedPnLPct < 0 ? '-' : '';
+
   return (
     <div className="flex flex-col items-end">
       <p
         className={`text-[13px] font-normal tracking-[-0.26px] text-right ${
           pnl.unrealisedPnLUSDC >= 0 ? 'text-[#4ADE80]' : 'text-[#EF4444]'
         }`}
       >
-        {pnl.unrealisedPnLUSDC >= 0 ? '+' : ''}$
+        {usdSign}$
         {formatExponentialSmallNumber(
           limitDigitsNumber(Math.abs(pnl.unrealisedPnLUSDC))
         )}
       </p>
       <p
         className={`text-xs font-normal tracking-[-0.24px] text-right ${
           pnl.unrealisedPnLPct >= 0 ? 'text-[#4ADE80]' : 'text-[#EF4444]'
         }`}
       >
-        {pnl.unrealisedPnLPct >= 0 ? '+' : ''}
-        {pnl.unrealisedPnLPct.toFixed(2)}%
+        {pctSign}
+        {Math.abs(pnl.unrealisedPnLPct).toFixed(2)}%
       </p>
     </div>
   );
src/utils/pnl.ts (3)

48-48: Timestamp unit inconsistency across functions.

As noted in a past review, reconstructTrades uses tx.timestamp as-is (Line 48, likely milliseconds from Mobula), while getRelayValidatedTrades divides by 1000 (Line 360). This inconsistency can cause sorting/comparison issues when trades from different sources are combined.

Consider standardizing on one unit (seconds or milliseconds) across all ReconstructedTrade producers.

Also applies to: 105-105, 360-360


131-143: Guard for negative inventory not fully addressed.

The check if (totalTokens === 0) return; only handles exact zero. If transaction history is incomplete and a SELL occurs when totalTokens is already negative from a previous iteration, division at Line 135 proceeds with a negative denominator, producing incorrect WAC.

     } else {
       // SELL
-      if (totalTokens === 0) return; // Selling without inventory (shouldn't happen in perfect history)
+      if (totalTokens <= 0) return; // Selling without inventory (shouldn't happen in perfect history)

311-344: Only checking inTxs for USDC state changes; outTxs may contain USDC for SELLs.

This code only iterates relayReq.data.inTxs, but for SELL transactions the USDC received may appear in outTxs. This could cause valid trades to be skipped or have usdcAmount = 0. Note that calculatePnLFromRelay (Lines 440-443) correctly processes both.

-      if (relayReq.data?.inTxs) {
-        relayReq.data.inTxs.forEach((inTx) => {
+      const allTxs = [
+        ...(relayReq.data?.inTxs || []),
+        ...(relayReq.data?.outTxs || []),
+      ];
+      
+      allTxs.forEach((tx) => {
+        if (tx.stateChanges) {
+          tx.stateChanges.forEach((stateChange) => {
🧹 Nitpick comments (11)
src/apps/pulse/components/Buy/BuyButton.tsx (1)

113-144: Consider using a cleaner type guard for response shape detection.

The current approach uses 'bids' in (expressIntentResponse || {}) to detect ExpressIntentResponse, which works but is somewhat fragile and requires a type assertion on line 134. A helper function or explicit type guard would make the intent clearer and improve type safety.

Consider refactoring to use a type guard function:

+function isExpressIntentResponse(
+  response: ExpressIntentResponse | BuyOffer | null | { error: string }
+): response is ExpressIntentResponse {
+  return response !== null && typeof response === 'object' && 'bids' in response;
+}
+
 const isDisabled = () => {
   if (isInstalling || isFetching) {
     return true;
   }
   if (notEnoughLiquidity) {
     return true;
   }
   if (!areModulesInstalled && payingTokens.length > 0) {
     return false;
   }
   // Check if it's an error object
   if ((expressIntentResponse as { error?: string })?.error) {
     return true;
   }
   // Check if it's ExpressIntentResponse with no bids
-  if ('bids' in (expressIntentResponse || {})) {
+  if (isExpressIntentResponse(expressIntentResponse)) {
     return (
       isLoading ||
       !token ||
       !(parseFloat(usdAmount) > 0) ||
       !expressIntentResponse ||
-      (expressIntentResponse as ExpressIntentResponse)?.bids?.length === 0
+      expressIntentResponse.bids?.length === 0
     );
   }
   // For BuyOffer or other types, just check basic conditions
   return (
     isLoading ||
     !token ||
     !(parseFloat(usdAmount) > 0) ||
     !expressIntentResponse
   );
 };
src/apps/pulse/components/Sell/Sell.tsx (1)

264-264: Remove unnecessary comment.

The comment "Calculate token balance (must be after getTokenBalance function)" doesn't add value since getTokenBalance is defined earlier in the same file (lines 206-240). The variable assignment is straightforward.

-  // Calculate token balance (must be after getTokenBalance function)
   const tokenBalance = getTokenBalance();
src/apps/pulse/components/Buy/Buy.tsx (1)

427-428: Remove duplicate comment.

The comment "// Refresh PnL data" appears without associated code and duplicates the comment at line 380. This appears to be a leftover from refactoring.

-    // Refresh PnL data
-
     try {
src/apps/pulse/components/Search/PortfolioTokenList.tsx (3)

160-164: PnL sort key is effectively a no-op.

The 'pnl' case sets valA and valB to 0, so clicking the "Unrealized PnL/%" header only toggles the sort indicator without changing row order. The comment acknowledges PnL is calculated in child components.

Consider either:

  1. Disabling the click handler and hiding the sort indicator for the PnL column until sorting is implemented
  2. Implementing PnL-based sorting by lifting PnL calculation to this component

416-419: Remove commented-out code.

These commented lines appear to be leftover debug/development code and should be removed for cleaner production code.

                     <p
                       className="text-xs font-normal tracking-[-0.24px] text-white opacity-50 text-left"
                       data-testid="pulse-portfolio-token-price"
                     >
-                      ${/* // Skip PnL for small balances */}
-                      {/* if (valueUSD < 0.01) { */}
-                      {/*   return { ...token, pnl: null }; */}
-                      {/* } */}
+                      $
                       {token.price

338-347: Skeleton rows shown alongside actual tokens during refresh.

When isFetching is true, skeleton rows are rendered (lines 339-347) AND actual tokens are shown with opacity-30 (line 359). This creates a confusing UI where both placeholders and dimmed content appear simultaneously.

Consider showing only one or the other:

       {/* Show skeleton during refresh */}
-      {isFetching && !isLoading && (
+      {isFetching && !isLoading && sortedTokens.length === 0 && (
         <>
           <SkeletonTokenRow />
           ...
         </>
       )}

Or remove the skeleton entirely during refresh since the opacity reduction already indicates loading state.

src/hooks/useTokenPnL.ts (1)

65-68: Redundant effect to update refetch in result.

The refetch function is already stable (wrapped in useCallback with empty deps), so updating it in result on every mount is unnecessary. The initial state already sets an empty function, and the main calculation effect could directly use the callback.

Consider removing this effect and initializing refetch directly in the initial state or updating it once during initialization.

src/services/relayApi.ts (2)

3-99: Large interface definition - consider consolidating with types/api.ts.

The RelayRequest interface spans ~100 lines and is defined here rather than in the central types/api.ts file where other API types reside. For consistency and maintainability, consider moving this to types/api.ts.


108-114: RTK Query's retry() wrapper already includes exponential backoff by default.

The configuration (2 retries, 5-second timeout) is reasonable for user-facing PnL calculations with 30s auto-refresh. However, consider adding a retryCondition to bail out early on non-transient errors (4xx client errors, validation failures) rather than exhausting all retries on permanent failures.

Example improvement:

const fetchBaseQueryWithRetry = retry(
  fetchBaseQuery({
    baseUrl: RELAY_API_URL,
    timeout: 5000,
  }),
  { 
    maxRetries: 2,
    retryCondition: (error) => error.status > 499 || error.status === 408,
  }
);
src/apps/pulse/components/PnLStats/PnLStats.tsx (2)

42-44: Remove duplicated comment.

The same comment appears three times consecutively.

   // Format PnL with sign and percentage: -$0.80 (-0.5%) or +$1.20 (+2.3%)
-  // Format PnL with sign and percentage: -$0.80 (-0.5%) or +$1.20 (+2.3%)
-  // Format PnL with sign and percentage: -$0.80 (-0.5%) or +$1.20 (+2.3%)
   const formatPnLWithPercent = (usdValue: number, pctValue: number) => {

34-40: Minor inconsistency in formatToken output.

For val === 0, this returns '0.00', but for other values (e.g., 1.50), trailing zeros are stripped to '1.5'. If consistency is desired, consider returning '0' for zero or keeping two decimals for all values.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65abc32 and f4cfa2a.

📒 Files selected for processing (18)
  • src/apps/pulse/components/Buy/Buy.tsx (7 hunks)
  • src/apps/pulse/components/Buy/BuyButton.tsx (3 hunks)
  • src/apps/pulse/components/PnLStats/PnLStats.tsx (1 hunks)
  • src/apps/pulse/components/PnLStats/PnLStatsSkeleton.tsx (1 hunks)
  • src/apps/pulse/components/PnLStats/StatGroup.tsx (1 hunks)
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx (5 hunks)
  • src/apps/pulse/components/Search/SkeletonTokenRow.tsx (1 hunks)
  • src/apps/pulse/components/Search/SortIndicator.tsx (1 hunks)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx (1 hunks)
  • src/apps/pulse/components/Sell/Sell.tsx (7 hunks)
  • src/apps/pulse/constants/tokens.ts (1 hunks)
  • src/hooks/useTokenPnL.ts (1 hunks)
  • src/services/pillarXApiWalletPortfolio.ts (1 hunks)
  • src/services/relayApi.ts (1 hunks)
  • src/services/relayApiAsync.ts (1 hunks)
  • src/store.ts (2 hunks)
  • src/utils/__tests__/pnl.test.ts (1 hunks)
  • src/utils/pnl.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/services/pillarXApiWalletPortfolio.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/store.ts
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/constants/tokens.ts
  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/PnLStats/PnLStats.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Buy/BuyButton.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
📚 Learning: 2025-05-23T14:44:33.911Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 315
File: src/apps/the-exchange/utils/wrappedTokens.ts:6-20
Timestamp: 2025-05-23T14:44:33.911Z
Learning: XDAI (Gnosis Chain) is intentionally excluded from the WRAPPED_NATIVE_TOKEN_ADDRESSES mapping in the exchange app's wrappedTokens utility.

Applied to files:

  • src/apps/pulse/constants/tokens.ts
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/utils/pnl.ts
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/PnLStats/PnLStats.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/hooks/useTokenPnL.ts
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
📚 Learning: 2025-11-04T14:34:00.373Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 445
File: src/containers/Authorized.tsx:63-77
Timestamp: 2025-11-04T14:34:00.373Z
Learning: In src/containers/Authorized.tsx, the tempKit useEffect intentionally excludes chainId from its dependency array because the kit is used by useWalletModeVerification to check wallet mode across all supported chains, regardless of the currently selected chainId. The chainId parameter is only used for initial kit configuration and does not affect the multi-chain verification results.

Applied to files:

  • src/hooks/useTokenPnL.ts
  • src/apps/pulse/components/Buy/Buy.tsx
📚 Learning: 2025-05-28T14:30:02.702Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 320
File: src/components/BottomMenuModal/HistoryModal/TransactionInfo.tsx:177-182
Timestamp: 2025-05-28T14:30:02.702Z
Learning: In the transaction history tracking feature, chain ID validation is performed earlier in the flow before the values reach the TransactionInfo component, so additional validation at the display level is not needed.

Applied to files:

  • src/hooks/useTokenPnL.ts
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required text field. The TileTitleProps interface aligns with the TokensMarketData.title type in api.ts which also has optional fields.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required title field. The TileTitleProps structure aligns with how it's used in the TokensMarketData type in api.ts.

Applied to files:

  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
🧬 Code graph analysis (9)
src/apps/pulse/components/Sell/Sell.tsx (3)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (35-310)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (10-112)
src/hooks/useTokenPnL.ts (4)
src/types/api.ts (2)
  • WalletTransactionsMobulaResponse (792-801)
  • PnLMetrics (815-826)
src/utils/pnl.ts (2)
  • calculatePnL (118-200)
  • calculatePnLFromRelay (378-545)
src/services/relayApi.ts (1)
  • RelayRequest (3-99)
src/services/relayApiAsync.ts (1)
  • fetchRelayRequestByHash (32-60)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
src/types/api.ts (1)
  • PnLMetrics (815-826)
src/apps/pulse/components/Buy/Buy.tsx (3)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (35-310)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (10-112)
src/services/relayApiAsync.ts (2)
src/services/relayApi.ts (3)
  • RelayRequest (3-99)
  • relayApi (116-163)
  • relayApi (165-169)
src/store.ts (1)
  • store (67-79)
src/apps/pulse/components/Search/PortfolioTokenList.tsx (4)
src/services/tokensData.ts (2)
  • Token (19-29)
  • chainNameToChainIdTokensData (234-255)
src/services/pillarXApiWalletPortfolio.ts (1)
  • convertPortfolioAPIResponseToToken (27-61)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/apps/pulse/components/Search/TokenPnLCell.tsx (1)
  • TokenPnLCell (18-92)
src/apps/pulse/components/Buy/BuyButton.tsx (1)
src/apps/pulse/hooks/useRelayBuy.ts (1)
  • BuyOffer (30-36)
src/apps/pulse/components/Search/TokenPnLCell.tsx (4)
src/services/tokensData.ts (1)
  • PortfolioToken (31-34)
src/types/api.ts (1)
  • WalletTransactionsMobulaResponse (792-801)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (35-310)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/utils/pnl.ts (4)
src/apps/pulse/constants/tokens.ts (1)
  • allStableCurrencies (3-11)
src/types/api.ts (3)
  • MobulaTransactionRow (768-790)
  • ReconstructedTrade (803-813)
  • PnLMetrics (815-826)
src/services/relayApi.ts (1)
  • RelayRequest (3-99)
src/services/relayApiAsync.ts (1)
  • fetchRelayRequestByHash (32-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (18)
src/apps/pulse/constants/tokens.ts (1)

3-15: Exporting allStableCurrencies looks fine; confirm intended usage vs STABLE_CURRENCIES.

Making allStableCurrencies public is a clean way to expose the raw mapping for consumers like PnL, while keeping STABLE_CURRENCIES as the feature‑gated view. Just make sure new call sites that care about the Gnosis toggle continue to use STABLE_CURRENCIES, and only use allStableCurrencies where bypassing isGnosisEnabled is explicitly desired (e.g., purely analytical/off‑UI logic).

src/utils/__tests__/pnl.test.ts (1)

1-211: LGTM! Comprehensive test coverage for PnL logic.

The test suite covers:

  • Trade reconstruction from buy/sell transactions
  • PnL calculation with realised and unrealised metrics
  • Edge case handling (SELL-only transactions)

The mock data structure now includes all required fields (hash, method_label, contract) addressing the previous review feedback.

src/apps/pulse/components/Sell/Sell.tsx (1)

82-122: LGTM! PnL integration properly typed without unsafe casts.

The PnL data flow is well-structured:

  • Fetches wallet transactions with proper skip logic
  • Finds matching portfolio token using useMemo
  • Constructs properly typed token object for useTokenPnL
  • Handles null cases gracefully

This addresses the previous concerns about type assertions.

src/apps/pulse/components/Buy/Buy.tsx (1)

170-208: LGTM! PnL integration properly implemented.

The PnL data flow correctly:

  • Captures wallet transactions query results (addresses previous review feedback)
  • Finds matching portfolio token with proper memoization
  • Constructs properly typed token object without unsafe type assertions
  • Integrates PnL refresh into the buy flow

The previous concerns about uncaptured query results and type assertions have been resolved.

src/services/relayApiAsync.ts (3)

1-30: LGTM - Well-structured async helpers for non-React contexts.

The separation to avoid circular dependencies with store.ts is a good architectural decision. The error handling is consistent, returning fallback values ([]) instead of throwing, which aligns with the original axios implementation as noted in the comments.


32-60: Good handling of 404 as expected case.

The special handling of 404 status for fetchRelayRequestByHash is appropriate since a transaction not being in Relay is an expected scenario, not an error condition. The type assertion on line 42 is necessary but acceptable given RTK Query's error shape.


62-79: LGTM - Consistent pattern with other helpers.

The error handling follows the same pattern as the other functions in this file.

src/apps/pulse/components/Search/TokenPnLCell.tsx (2)

18-45: LGTM - Clean conditional hook pattern.

The conditional passing of null to useTokenPnL when token or walletAddress is missing is a clean pattern that avoids hook violations while skipping unnecessary calculations.


47-51: The refetch dependency is properly managed and won't fire repeatedly.

isRefreshing is correctly toggled back to false in the finally block of the async handleRefresh() operation (HomeScreen.tsx:369), so the effect won't trigger repeatedly. The refetch function from useTokenPnL is memoized with useCallback and has no dependencies, ensuring it remains stable across renders. No ref tracking or transition detection is needed here.

src/hooks/useTokenPnL.ts (2)

120-132: Minimum loading duration for UX is a nice touch.

The 100ms minimum loading duration ensures skeleton animations are visible and prevents UI flicker. This is a thoughtful UX consideration.


281-294: Good dependency array management.

The eslint-disable comment is justified here - the specific primitive dependencies are correctly listed to avoid infinite loops from unstable object references. The comment on line 284 clearly explains why token is excluded.

src/services/relayApi.ts (2)

146-160: Good defensive handling of heterogeneous response shapes.

The transformResponse correctly handles both array responses and object-with-requests responses from the API. This defensive coding prevents runtime errors from API inconsistencies.


116-140: LGTM - Clean RTK Query endpoint definitions.

The endpoints are well-structured with appropriate query parameters and transform responses. The use of status = 'filled' as default for getRelayRequests aligns with PnL calculation needs (only completed transactions matter).

src/apps/pulse/components/PnLStats/PnLStats.tsx (1)

1-112: Component structure looks good.

The component properly handles loading state, destructures metrics with sensible defaults, and the formatPnLWithPercent implementation correctly handles sign display for both positive and negative values. The StatGroup abstraction keeps the layout clean.

src/utils/pnl.ts (4)

15-19: Good consolidation of USDC addresses.

Using allStableCurrencies as the single source of truth addresses the previous duplication concern.


437-466: Good handling of both inTxs and outTxs in the fallback strategy.

The state-change fallback correctly combines both transaction arrays, addressing the USDC extraction concern for this code path.


503-528: Good sanity checks for data integrity.

The validation thresholds (USDC < $1T, exec price < $1M, positive price) provide sensible guardrails against malformed Relay data corrupting PnL calculations.


328-331: Potential precision loss with parseFloat on large integer strings.

balanceDiff values from Relay can be very large integers (wei). parseFloat loses precision for integers > 2^53. After division by decimals the impact is typically minimal, but for very large trades this could introduce small errors.

Based on learnings, the team has accepted JavaScript's floating-point limitations as a valid trade-off.

Also applies to: 453-453

@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) December 2, 2025 15:04 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/apps/pulse/components/Search/Search.tsx (1)

671-681: Add keyboard accessibility to the toggle.

The toggle div lacks keyboard navigation support. Add role="checkbox", tabIndex={0}, aria-checked, and onKeyDown to make it accessible to keyboard users.

Apply this diff to improve accessibility:

-                      <div
-                        className="flex items-center gap-1.5 cursor-pointer opacity-50 hover:opacity-100 transition-opacity"
-                        onClick={() => setHideSmallBalances(!hideSmallBalances)}
-                      >
+                      <div
+                        className="flex items-center gap-1.5 cursor-pointer opacity-50 hover:opacity-100 transition-opacity"
+                        onClick={() => setHideSmallBalances(!hideSmallBalances)}
+                        onKeyDown={(e) => {
+                          if (e.key === 'Enter' || e.key === ' ') {
+                            e.preventDefault();
+                            setHideSmallBalances(!hideSmallBalances);
+                          }
+                        }}
+                        role="checkbox"
+                        aria-checked={hideSmallBalances}
+                        tabIndex={0}
+                        aria-label="Hide balances under $0.5"
+                      >
src/apps/pulse/components/Buy/BuyButton.tsx (1)

144-178: Simplify isDisabled union handling and make the bids guard clearer

The new error/bids handling works, but there are two opportunities to tighten it up:

  • Error is checked twice: once via optional chaining and again in the fallback branch via 'error' in expressIntentResponse. You can rely on the first check and drop the second for clarity.
  • 'bids' in (expressIntentResponse || {}) is a bit opaque; a direct expressIntentResponse && 'bids' in expressIntentResponse guard is easier to read and keeps the 'in' operator clearly on a non-null object, avoiding the need for the extra !expressIntentResponse check inside that branch.

A refactor that preserves behavior could look like:

-    // Check if it's an error object
-    if ((expressIntentResponse as { error?: string })?.error) {
-      return true;
-    }
-    // Check if it's ExpressIntentResponse with no bids
-    if ('bids' in (expressIntentResponse || {})) {
+    // Check if it's an error object
+    const errorMessage = (
+      expressIntentResponse as { error?: string } | null | undefined
+    )?.error;
+    if (errorMessage) {
+      return true;
+    }
+
+    // ExpressIntentResponse branch – guard on truthy object before `'in'`
+    if (expressIntentResponse && 'bids' in expressIntentResponse) {
       return (
         isLoading ||
         !token ||
         !(parseFloat(usdAmount) > 0) ||
-        !expressIntentResponse ||
         (expressIntentResponse as ExpressIntentResponse)?.bids?.length === 0
       );
     }
-    // For BuyOffer or other types, just check basic conditions
+    // For BuyOffer or other types, just check basic conditions
     return (
       isLoading ||
       !token ||
       !(parseFloat(usdAmount) > 0) ||
-      !expressIntentResponse ||
-      ('error' in expressIntentResponse && !!expressIntentResponse.error) ||
-      ('bids' in expressIntentResponse &&
-        (expressIntentResponse as ExpressIntentResponse)?.bids?.length === 0)
+      !expressIntentResponse
     );

This keeps the special ExpressIntentResponse-with-bids behavior while reducing duplication and making the control flow over the union shapes easier to follow.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4cfa2a and 87cb93d.

📒 Files selected for processing (6)
  • src/apps/pulse/components/App/tests/AppWrapper.test.tsx (1 hunks)
  • src/apps/pulse/components/Buy/Buy.tsx (7 hunks)
  • src/apps/pulse/components/Buy/BuyButton.tsx (2 hunks)
  • src/apps/pulse/components/Search/Search.tsx (5 hunks)
  • src/apps/pulse/components/Search/tests/Search.test.tsx (16 hunks)
  • src/apps/pulse/components/Sell/Sell.tsx (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/apps/pulse/components/App/tests/AppWrapper.test.tsx
  • src/apps/pulse/components/Search/tests/Search.test.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Search/Search.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Search/Search.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/apps/pulse/components/Search/Search.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: Cloudflare Pages: pillarx-debug
  • GitHub Check: build
🔇 Additional comments (9)
src/apps/pulse/components/Search/Search.tsx (3)

17-17: LGTM!

The new imports are correctly used: useIsMobile on line 521 and filterMarketsByLiquidity on line 172. Import reordering is cosmetic.

Also applies to: 26-26, 53-54


107-107: LGTM!

The hideSmallBalances state with a default value of true provides a sensible default UX—hiding small balances initially and allowing users to toggle visibility.


856-856: LGTM!

The hideSmallBalances prop is correctly passed to PortfolioTokenList, enabling the filtering logic downstream.

src/apps/pulse/components/Sell/Sell.tsx (5)

7-7: LGTM!

The new imports for PnL functionality are appropriate and all are utilized in the component.

Also applies to: 34-34, 38-39, 43-43


82-89: LGTM!

Transaction fetching is properly implemented with the skip parameter preventing unnecessary queries when the wallet address is unavailable.


91-100: LGTM!

The portfolio token lookup is properly memoized with correct dependencies. The matching logic correctly compares both contract address and chain ID to handle multi-chain scenarios.


124-129: LGTM!

The refetch effect correctly responds to the parent component's refresh trigger with appropriate dependencies and safe optional chaining.


631-642: LGTM!

The conditional rendering logic for PnLStats is well-designed. It appropriately shows the component when loading or when there's actual transaction history (bought or sold > 0), which avoids displaying empty PnL data for tokens without transaction history.

src/apps/pulse/components/Buy/BuyButton.tsx (1)

15-43: Chain name import and CTA usage look good

Importing getChainName and using it in the "Enable Trading on ..." CTA is consistent and safely guarded by the payingToken check; no issues here.

@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) December 2, 2025 15:17 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/apps/pulse/components/Buy/Buy.tsx (1)

433-434: Remove orphaned comment.

The comment "// Refresh PnL data" on line 433 appears to be leftover from editing. The actual refetchPnL() call is already handled earlier at lines 386-389.

-    // Refresh PnL data
-
     try {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87cb93d and 310b22a.

📒 Files selected for processing (6)
  • src/apps/pulse/components/App/HomeScreen.tsx (3 hunks)
  • src/apps/pulse/components/Buy/Buy.tsx (9 hunks)
  • src/apps/pulse/components/Search/Search.tsx (5 hunks)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx (1 hunks)
  • src/apps/pulse/components/Search/tests/Search.test.tsx (15 hunks)
  • src/apps/pulse/components/Sell/Sell.tsx (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/App/HomeScreen.tsx
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/App/HomeScreen.tsx
  • src/apps/pulse/components/Search/Search.tsx
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Search/tests/Search.test.tsx
  • src/apps/pulse/components/App/HomeScreen.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Search/tests/Search.test.tsx
  • src/apps/pulse/components/App/HomeScreen.tsx
  • src/apps/pulse/components/Search/Search.tsx
📚 Learning: 2025-11-04T14:34:00.373Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 445
File: src/containers/Authorized.tsx:63-77
Timestamp: 2025-11-04T14:34:00.373Z
Learning: In src/containers/Authorized.tsx, the tempKit useEffect intentionally excludes chainId from its dependency array because the kit is used by useWalletModeVerification to check wallet mode across all supported chains, regardless of the currently selected chainId. The chainId parameter is only used for initial kit configuration and does not affect the multi-chain verification results.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
🧬 Code graph analysis (3)
src/apps/pulse/components/Sell/Sell.tsx (3)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (35-310)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (10-112)
src/apps/pulse/components/Buy/Buy.tsx (3)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (35-310)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (10-112)
src/apps/pulse/components/Search/tests/Search.test.tsx (2)
src/theme/index.ts (1)
  • defaultTheme (83-163)
src/apps/pulse/components/Search/Search.tsx (1)
  • Search (82-876)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (12)
src/apps/pulse/components/Sell/Sell.tsx (3)

82-100: PnL data wiring looks correct.

The transactions fetching with skip option and portfolio token matching via useMemo are properly implemented. The chain ID comparison using getChainId ensures accurate matching between portfolio tokens and the selected token.


103-129: PnL hook integration and refresh logic are well-structured.

The useTokenPnL hook is invoked with properly typed props (no as any casts), and the refresh effect correctly triggers refetchPnL when the parent's isRefreshing changes. The conditional null pattern for missing prerequisites is appropriate.


631-642: PnL display conditions are appropriate.

The conditional rendering logic correctly shows PnLStats only when:

  1. A token is selected, AND
  2. Either loading is in progress OR there's actual PnL data (buys or sells)

This prevents showing an empty PnL component when there's no trading history.

src/apps/pulse/components/Buy/Buy.tsx (2)

174-212: PnL integration follows consistent patterns with Sell.tsx.

The transactions fetching, portfolio token lookup, and useTokenPnL hook usage mirror the implementation in Sell.tsx. The properly typed token object construction (without as any casts) addresses the concerns from previous reviews.


862-873: PnLStats rendering logic is consistent with Sell.tsx.

The conditional display logic and loading state combination (isPnLLoading || isTransactionsLoading || isRefreshing) maintain consistency across both Buy and Sell components.

src/apps/pulse/components/App/HomeScreen.tsx (1)

1143-1143: Correctly forwards refresh state to Buy component.

The isRefreshing={isRefreshingHome} prop enables the Buy component to synchronize its PnL loading state with the parent's refresh cycle, maintaining UI consistency.

src/apps/pulse/components/Search/Search.tsx (3)

107-108: State initialization for hideSmallBalances is appropriate.

Defaulting to true aligns with user expectations to hide dust/small balances in the portfolio view by default.


662-682: Toggle UI implementation for hiding small balances.

The toggle provides a clear visual indicator (filled/empty circle) and reasonable labeling. The click handler correctly inverts the state.

One minor UX consideration: the click target could be expanded to include the text label for easier interaction, but the current implementation is functional.


856-857: Props correctly passed to PortfolioTokenList.

Both hideSmallBalances and isFetching (mapped from walletPortfolioFetching) are forwarded, enabling the child component to filter tokens and show loading skeletons appropriately.

src/apps/pulse/components/Search/tests/Search.test.tsx (3)

27-57: Mocks are properly structured for the new PnL dependencies.

The mocks for useTokenPnL, useGetWalletTransactionsQuery, and useTransactionKit return appropriate default values that match the expected hook return types. The pillarXApiWalletTransactions mock includes the RTK Query service structure (reducerPath, reducer, middleware).


130-136: Good test utility pattern with renderWithProviders.

The helper consolidates ThemeProvider and MemoryRouter wrapping, reducing boilerplate across tests and ensuring consistent provider setup.


325-333: Test correctly targets ESC button in sell mode.

The updated test properly uses isBuy={false} since the ESC close button (with data-testid="pulse-search-esc-button") only renders in non-buy mode. This aligns with the component's conditional rendering logic.

},
}));

const relayResults = await Promise.all(
Copy link
Collaborator

Choose a reason for hiding this comment

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

@IAmKio @aldin4u just a bit worried here as we are calling the relay api 20 times per token! Is there not a risk of being rate limited there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Don't think this is an issue since its user calling it and we are reducing the amount of calls by filtering. If the user gets rate limited, it would be great problem to address since they are trading so much :)

@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) December 2, 2025 15:55 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/utils/pnl.ts (2)

118-200: Handle over‑sells/negative inventory more explicitly in calculatePnL

The WAC logic is correct for “perfect” histories, but if the trade history is truncated or a user sells more than your reconstructed inventory, totalTokens can go negative. You clamp totalTokens/totalCostUSDC back to 0 at the end, but realised PnL and totalCostBasisSold will already have included cost basis for non‑existent tokens, which can materially skew PnL.

To make this more robust against incomplete history:

  • Guard against totalTokens <= 0 before computing WAC.
  • Clamp the sell size to available inventory so balances never go negative.
  • Scale the USDC proceeds proportionally when clamping, so realised PnL remains consistent with the partial fill.

For example:

    } else {
      // SELL
-      if (totalTokens === 0) return; // Selling without inventory (shouldn't happen in perfect history)
-
-      const wac = totalCostUSDC / totalTokens;
-      const costBasis = trade.amountToken * wac;
-
-      totalTokens -= trade.amountToken;
-      totalCostUSDC -= costBasis;
-
-      totalCostBasisSold += costBasis;
-      realisedPnLUSDC += trade.amountQuoteUSDC - costBasis;
+      if (totalTokens <= 0) {
+        // No inventory left – skip trades that cannot be reconciled with history
+        return;
+      }
+
+      const wac = totalCostUSDC / totalTokens;
+
+      // Clamp sell size to available inventory to avoid negative balances
+      const sellAmount = Math.min(trade.amountToken, totalTokens);
+      const proportion = sellAmount / trade.amountToken;
+
+      const costBasis = sellAmount * wac;
+
+      totalTokens -= sellAmount;
+      totalCostUSDC -= costBasis;
+
+      totalCostBasisSold += costBasis;
+      realisedPnLUSDC += trade.amountQuoteUSDC * proportion - costBasis;
    }

This keeps balances non‑negative while still deriving a sensible realised PnL from partial history.


258-371: Include Relay outTxs when extracting USDC state changes in getRelayValidatedTrades

Here you only scan relayReq.data.inTxs for USDC stateChanges. For SELLs, the USDC leg can appear in outTxs instead, which means:

  • hasUSDCInRelay stays false and the trade is dropped entirely, even though it’s a valid Pulse trade.
  • You never reach the price fallback because you return early when !hasUSDCInRelay.

To align with calculatePnLFromRelay (which already combines inTxs and outTxs) and avoid missing trades, you should process both:

-      // For USDC amount, extract it from Relay stateChanges
-      usdcAmount = 0;
-      if (relayReq.data?.inTxs) {
-        relayReq.data.inTxs.forEach((inTx) => {
-          if (inTx.stateChanges) {
-            inTx.stateChanges.forEach((stateChange) => {
+      // For USDC amount, extract it from Relay stateChanges (in + out)
+      usdcAmount = 0;
+      const allTxs = [
+        ...(relayReq.data?.inTxs || []),
+        ...(relayReq.data?.outTxs || []),
+      ];
+
+      allTxs.forEach((tx) => {
+        if (tx.stateChanges) {
+          tx.stateChanges.forEach((stateChange) => {
             const tokenAddr =
               stateChange.change?.data?.tokenAddress?.toLowerCase();
             const changeAddress = stateChange.address?.toLowerCase();
@@
-              }
-            });
-          }
-        });
-      }
+              }
+            });
+          }
+        }
+      });

Without this, you’ll systematically under‑report PnL for SELLs whose USDC flow is only visible in outTxs.

🧹 Nitpick comments (2)
src/utils/pnl.ts (2)

15-19: Clarify USDC_ADDRESSES coupling to allStableCurrencies for future safety

USDC_ADDRESSES is built by blindly mapping allStableCurrencies. Today that list contains only USDC addresses, but if other stables (USDT/DAI, etc.) are ever added there, this module will start treating them as USDC (including hard‑coding 1e6 decimals in several places), which will silently skew PnL.

I’d recommend either:

  • Introducing a dedicated USDC‑only export in tokens.ts (e.g. usdcCurrencies / usdcAddresses) and using that here, or
  • Renaming allStableCurrencies to something USDC‑specific if it is intended to be USDC‑only long‑term, so the intent is explicit.

21-116: Minor: use asset.contract as a fallback for tokenAddress

reconstructTrades looks solid overall (grouping by tx_hash, USDC by address, and direction inference are all consistent), but tokenAddress is set to tx.asset.contracts?.[0] || ''. That can leave you with an empty token address even though Mobula exposes asset.contract.

Using the same fallback you already use for txContract keeps things consistent and avoids empty addresses:

-      } else {
-        tokenSymbol = symbol;
-        tokenAddress = tx.asset.contracts?.[0] || '';
+      } else {
+        tokenSymbol = symbol;
+        tokenAddress =
+          (tx.asset.contracts && tx.asset.contracts[0]) || tx.asset.contract;
         if (isInbound) tokenChange += amount;
         if (isOutbound) tokenChange -= amount;
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 310b22a and c8a9bec.

📒 Files selected for processing (1)
  • src/utils/pnl.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/utils/pnl.ts
🧬 Code graph analysis (1)
src/utils/pnl.ts (4)
src/apps/pulse/constants/tokens.ts (1)
  • allStableCurrencies (3-11)
src/types/api.ts (3)
  • MobulaTransactionRow (768-790)
  • ReconstructedTrade (803-813)
  • PnLMetrics (815-826)
src/services/relayApi.ts (1)
  • RelayRequest (3-99)
src/services/relayApiAsync.ts (1)
  • fetchRelayRequestByHash (32-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Cloudflare Pages: pillarx-debug
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build

@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) December 2, 2025 16:49 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/utils/pnl.ts (2)

118-200: Oversell scenario still skews PnL; consider clamping sell size to available inventory

The totalTokens <= 0 guard (Line 133) prevents division by zero/negative in wac, but if a SELL trade’s amountToken exceeds totalTokens, you still:

  • Compute wac using the full current inventory.
  • Subtract trade.amountToken (Line 138), driving totalTokens negative.
  • Clamp totalTokens/totalCostUSDC back to zero later (Lines 146-148), leaving realisedPnLUSDC and totalCostBasisSold overstated for incomplete histories.

For more robust behaviour when history is incomplete or data is slightly off, clamping the sell amount to available inventory (and optionally logging) avoids negative inventory and keeps realised PnL consistent:

    } else {
      // SELL
      if (totalTokens <= 0) return; // Selling without inventory (handles zero and negative cases)

-      const wac = totalCostUSDC / totalTokens;
-      const costBasis = trade.amountToken * wac;
-
-      totalTokens -= trade.amountToken;
+      const wac = totalCostUSDC / totalTokens;
+      const sellAmount = Math.min(trade.amountToken, totalTokens);
+      const costBasis = sellAmount * wac;
+
+      if (sellAmount < trade.amountToken) {
+        // Optional: log or track that history is incomplete / oversell occurred
+        // console.warn('[calculatePnL] Sell amount exceeds inventory; clamping to available balance');
+      }
+
+      totalTokens -= sellAmount;
       totalCostUSDC -= costBasis;
 
       totalCostBasisSold += costBasis;
-      realisedPnLUSDC += trade.amountQuoteUSDC - costBasis;
+      realisedPnLUSDC += trade.amountQuoteUSDC * (sellAmount / trade.amountToken) - costBasis;

This keeps state internally consistent even when the upstream trade stream is imperfect.


207-372: Relay USDC extraction still ignores outTxs; align with calculatePnLFromRelay

This function currently only inspects relayReq.data.inTxs when extracting USDC state changes (Lines 311-343). For SELL flows, the USDC leg is often recorded in outTxs, so you can miss USDC deltas and incorrectly drop valid trades when hasUSDCInRelay stays false.

You already handle this correctly in calculatePnLFromRelay by combining inTxs and outTxs. Mirroring that pattern here would make the behaviour consistent and fix missing USDC for some trades:

-      // For USDC amount, extract it from Relay stateChanges
-      usdcAmount = 0;
-      if (relayReq.data?.inTxs) {
-        relayReq.data.inTxs.forEach((inTx) => {
-          if (inTx.stateChanges) {
-            inTx.stateChanges.forEach((stateChange) => {
+      // For USDC amount, extract it from Relay stateChanges
+      usdcAmount = 0;
+      const allTxs = [
+        ...(relayReq.data?.inTxs || []),
+        ...(relayReq.data?.outTxs || []),
+      ];
+
+      allTxs.forEach((tx) => {
+        if (tx.stateChanges) {
+          tx.stateChanges.forEach((stateChange) => {
             const tokenAddr =
               stateChange.change?.data?.tokenAddress?.toLowerCase();
             const changeAddress = stateChange.address?.toLowerCase();
@@
-            });
-          }
-        });
-      }
+          });
+        }
+      });

Given calculatePnLFromRelay already uses the combined-array approach, you might also consider extracting a small helper for “USDC delta from Relay stateChanges” to keep the logic DRY between the two paths.

🧹 Nitpick comments (1)
src/utils/pnl.ts (1)

378-551: Solid Relay-based reconstruction; consider renaming and small DRY/refinement

The overall structure here looks good: prefer metadata, fall back to state changes, normalise timestamps to seconds, use price fallback, and guard against absurd values. A few small points:

  1. Function name vs return type (clarity)
    calculatePnLFromRelay returns ReconstructedTrade[], not PnLMetrics. That’s slightly misleading alongside calculatePnL. If API stability allows it, consider renaming to something like reconstructTradesFromRelay or buildRelayTrades to better reflect its role.

  2. Share state-change parsing with getRelayValidatedTrades
    The state-change path here (Lines 438-492) and the USDC extraction logic in getRelayValidatedTrades are conceptually similar: both walk Relay state changes, aggregate token and USDC deltas, and normalise timestamps. Pulling the common part into a shared helper (e.g. extractRelayTokenAndUsdcDeltas(req, tokenContract)) would reduce duplication and help keep future fixes in sync.

  3. Big-number precision (optional)
    Using parseFloat and 10 ** token.decimals is pragmatic but will lose precision for very large on-chain integers (especially decimals >= 18). If you expect high-volume or very precise PnL, migrating to a bigint/BN library for Relay amounts would improve numerical stability, though it’s not blocking for typical retail-sized flows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8a9bec and 8d193ae.

📒 Files selected for processing (1)
  • src/utils/pnl.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/utils/pnl.ts
🧬 Code graph analysis (1)
src/utils/pnl.ts (4)
src/apps/pulse/constants/tokens.ts (1)
  • allStableCurrencies (3-11)
src/types/api.ts (3)
  • MobulaTransactionRow (768-790)
  • ReconstructedTrade (803-813)
  • PnLMetrics (815-826)
src/services/relayApi.ts (1)
  • RelayRequest (3-99)
src/services/relayApiAsync.ts (1)
  • fetchRelayRequestByHash (32-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (1)
src/utils/pnl.ts (1)

15-19: Centralising USDC addresses via allStableCurrencies looks good

Deriving USDC_ADDRESSES from allStableCurrencies removes duplication and keeps the address set consistent across consumers; the lowercasing here also simplifies comparisons later. No issues from my side.

@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) December 3, 2025 14:36 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/apps/pulse/components/Search/PortfolioTokenList.tsx (1)

83-132: Consolidate sorting logic and avoid a no-op PnL sort header.

Currently tokens are sorted twice (inside getFilteredPortfolioTokens and again in sortedTokens), and the 'pnl' branch sets both comparison values to 0, so clicking “Unrealized PnL/%” only flips the indicator without changing row order. It would be cleaner to keep getFilteredPortfolioTokens for filtering only and let sortedTokens be the single source of sorting, and either implement a real PnL-based sort once metrics are available or disable the PnL header click until then to avoid a misleading UX.

Also applies to: 152-181, 283-289, 330-343

🧹 Nitpick comments (3)
src/apps/pulse/components/Buy/Buy.tsx (1)

395-401: Remove leftover PnL refresh comment to avoid confusion.

There’s a “Refresh PnL data” comment in refreshBuyIntent without corresponding logic now that the actual refresh call is earlier in the function; consider deleting it to keep the intent clear.

Also applies to: 444-445

src/services/relayApiAsync.ts (1)

9-30: Consider disabling subscriptions for one-shot Relay helpers.

fetchRelayRequests and fetchRelayRequestsByUser don’t pass subscribe: false or call unsubscribe(), so each call leaves a live subscription even though you only need the resolved data; aligning these with fetchRelayRequestByHash by using subscribe: false would avoid unnecessary subscribers while still leveraging RTK Query’s caching.

Also applies to: 64-80

src/apps/pulse/components/Sell/Sell.tsx (1)

95-103: Optional: extract shared portfolioToken lookup used in Buy/Sell.

The contract+chainId matching logic to find portfolioToken is duplicated here and in Buy; consider a small shared helper to reduce drift if that matching rule ever changes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d193ae and 690da6c.

📒 Files selected for processing (6)
  • src/apps/pulse/components/Buy/Buy.tsx (9 hunks)
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx (5 hunks)
  • src/apps/pulse/components/Search/TokenPnLCell.tsx (1 hunks)
  • src/apps/pulse/components/Sell/Sell.tsx (7 hunks)
  • src/hooks/useTokenPnL.ts (1 hunks)
  • src/services/relayApiAsync.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-08-12T07:42:24.656Z
Learnt from: IAmKio
Repo: pillarwallet/x PR: 351
File: src/apps/pulse/utils/intent.ts:44-53
Timestamp: 2025-08-12T07:42:24.656Z
Learning: In the Pulse app's intent utilities (src/apps/pulse/utils/intent.ts), the team has chosen to use floating-point arithmetic for token amount calculations despite potential precision issues, accepting JavaScript's decimal place limitations as a valid trade-off for their use case.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
📚 Learning: 2025-11-21T13:10:33.422Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 461
File: src/apps/pulse/components/Search/MarketList.tsx:10-15
Timestamp: 2025-11-21T13:10:33.422Z
Learning: In the Pulse app's MarketList component (src/apps/pulse/components/Search/MarketList.tsx), markets should display liquidity (not price) in the right-hand column. This is per the design specification.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
  • src/hooks/useTokenPnL.ts
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/apps/pulse/components/Sell/Sell.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-11-04T14:34:00.373Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 445
File: src/containers/Authorized.tsx:63-77
Timestamp: 2025-11-04T14:34:00.373Z
Learning: In src/containers/Authorized.tsx, the tempKit useEffect intentionally excludes chainId from its dependency array because the kit is used by useWalletModeVerification to check wallet mode across all supported chains, regardless of the currently selected chainId. The chainId parameter is only used for initial kit configuration and does not affect the multi-chain verification results.

Applied to files:

  • src/apps/pulse/components/Buy/Buy.tsx
  • src/hooks/useTokenPnL.ts
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required text field. The TileTitleProps interface aligns with the TokensMarketData.title type in api.ts which also has optional fields.

Applied to files:

  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-04-23T15:04:20.826Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 290
File: src/apps/pillarx-app/components/TileTitle/TitleTitle.tsx:6-10
Timestamp: 2025-04-23T15:04:20.826Z
Learning: In this repository, TileTitleProps and TileTitle are different types that serve different purposes. TileTitleProps is used for the TileTitle component and has optional fields (title?, leftDecorator?, rightDecorator?), while TileTitle in api.ts has a required title field. The TileTitleProps structure aligns with how it's used in the TokensMarketData type in api.ts.

Applied to files:

  • src/apps/pulse/components/Search/TokenPnLCell.tsx
  • src/apps/pulse/components/Search/PortfolioTokenList.tsx
📚 Learning: 2025-05-28T14:30:02.702Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 320
File: src/components/BottomMenuModal/HistoryModal/TransactionInfo.tsx:177-182
Timestamp: 2025-05-28T14:30:02.702Z
Learning: In the transaction history tracking feature, chain ID validation is performed earlier in the flow before the values reach the TransactionInfo component, so additional validation at the display level is not needed.

Applied to files:

  • src/hooks/useTokenPnL.ts
🧬 Code graph analysis (5)
src/services/relayApiAsync.ts (2)
src/services/relayApi.ts (3)
  • RelayRequest (3-99)
  • relayApi (116-163)
  • relayApi (165-169)
src/store.ts (1)
  • store (67-79)
src/apps/pulse/components/Buy/Buy.tsx (3)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (35-339)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (10-112)
src/apps/pulse/components/Sell/Sell.tsx (3)
src/apps/pulse/utils/constants.ts (1)
  • getChainId (28-49)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (35-339)
src/apps/pulse/components/PnLStats/PnLStats.tsx (1)
  • PnLStats (10-112)
src/apps/pulse/components/Search/TokenPnLCell.tsx (4)
src/services/tokensData.ts (1)
  • PortfolioToken (31-34)
src/types/api.ts (1)
  • WalletTransactionsMobulaResponse (792-801)
src/hooks/useTokenPnL.ts (1)
  • useTokenPnL (35-339)
src/utils/number.tsx (2)
  • formatExponentialSmallNumber (119-149)
  • limitDigitsNumber (42-69)
src/hooks/useTokenPnL.ts (4)
src/types/api.ts (2)
  • WalletTransactionsMobulaResponse (792-801)
  • PnLMetrics (815-826)
src/utils/pnl.ts (2)
  • calculatePnL (118-200)
  • calculatePnLFromRelay (378-551)
src/services/relayApi.ts (1)
  • RelayRequest (3-99)
src/services/relayApiAsync.ts (1)
  • fetchRelayRequestByHash (32-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
src/apps/pulse/components/Buy/Buy.tsx (1)

174-215: PnL integration and refresh flow in Buy are wired correctly.

Transactions query, portfolio token lookup, useTokenPnL props, and the PnLStats visibility/loading conditions all line up with the hook’s contract and isRefreshing behavior; I don’t see functional issues here.

Also applies to: 217-223, 873-883

src/apps/pulse/components/Sell/Sell.tsx (1)

82-103: Sell-side PnL wiring and refresh behavior look sound.

Wallet transactions, portfolio token resolution, useTokenPnL props, the isRefreshingrefetchTransactions effect, and the PnLStats visibility/loading logic are consistent with the Buy flow and the hook’s expectations.

Also applies to: 105-121, 123-129, 631-640

src/apps/pulse/components/Search/PortfolioTokenList.tsx (1)

53-73: Wallet transaction refresh and per-row TokenPnLCell usage look correct.

Using useTransactionKit + useGetWalletTransactionsQuery with skip, refetching on isFetching, showing SkeletonTokenRow placeholders, and passing transactionsData, walletAddress, chainId, and isRefreshing={isFetching} into TokenPnLCell form a consistent, predictable PnL data flow for each portfolio row.

Also applies to: 347-356, 358-478

src/apps/pulse/components/Search/TokenPnLCell.tsx (1)

9-80: TokenPnLCell’s useTokenPnL integration and states are well-structured.

Props passed into useTokenPnL match the hook’s contract, and the loading (isRefreshing || isLoading), empty (!pnl), and populated render paths give a clear, consistent PnL cell without obvious edge-case issues.

src/hooks/useTokenPnL.ts (1)

35-339: useTokenPnL’s calculation, refresh, and safety guards look solid.

The hook correctly validates inputs, filters relevant Mobula transactions, limits Relay calls, reconstructs trades, adjusts metrics for actual wallet balance, and uses isMounted, refined dependencies, and a 30s interval + refetch trigger to manage loading and refresh without obvious race conditions or leaks.

- Changed trade detection to check state changes first before metadata
- Fixes issue where LINK sells were hidden in state changes while metadata showed USDC bridge
- Ensures all token movements are captured in complex multi-step transactions
- Verified fix detects previously missed LINK sell (0.039147 LINK for $0.01)
@github-actions github-actions bot temporarily deployed to Preview (feature-pnl) December 3, 2025 15:30 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/utils/pnl.ts (1)

21-116: Address unresolved issues: missing token address fallback and repeated lowercasing.

Two previously flagged issues remain unaddressed:

  1. Missing fallback to tx.asset.contract for token address (Line 78): The USDC detection logic (Lines 63-64) correctly falls back to tx.asset.contract, but tokenAddress assignment only uses tx.asset.contracts?.[0]. For tokens that only populate asset.contract, this results in an empty string in the reconstructed trade.

  2. Repeated walletAddress.toLowerCase() calls (Lines 49-50): Computing walletAddress.toLowerCase() on every iteration is inefficient and reduces readability.

Apply this diff to address both issues:

 export const reconstructTrades = (
   transactions: MobulaTransactionRow[],
   walletAddress: string
 ): ReconstructedTrade[] => {
   const trades: ReconstructedTrade[] = [];
   const groupedByTxHash: { [txHash: string]: MobulaTransactionRow[] } = {};
+  const walletAddressLc = walletAddress.toLowerCase();

   // Group by txHash
   transactions.forEach((tx) => {
     if (!groupedByTxHash[tx.tx_hash]) {
       groupedByTxHash[tx.tx_hash] = [];
     }
     groupedByTxHash[tx.tx_hash].push(tx);
   });

   // Process each group
   Object.keys(groupedByTxHash).forEach((txHash) => {
     const group = groupedByTxHash[txHash];
     let usdcChange = 0;
     let tokenChange = 0;
     let tokenSymbol = '';
     let tokenAddress = '';
     const feeUsd = 0;
     let timestamp = 0;

     // Identify assets and calculate net changes
     group.forEach((tx) => {
       timestamp = tx.timestamp; // Assume all rows have same timestamp or close enough
-      const isInbound = tx.to.toLowerCase() === walletAddress.toLowerCase();
-      const isOutbound = tx.from.toLowerCase() === walletAddress.toLowerCase();
+      const isInbound = tx.to.toLowerCase() === walletAddressLc;
+      const isOutbound = tx.from.toLowerCase() === walletAddressLc;

       if (!isInbound && !isOutbound) return; // Not related to wallet directly? (Maybe fee payer?)

       const { amount } = tx;
       const { symbol } = tx.asset;

       if (tx.type === 'native') {
         // Gas fee usually
         return;
       }

       // Check if this is a USDC transaction by matching address
       const txContract =
         (tx.asset.contracts && tx.asset.contracts[0]) || tx.asset.contract;
       const isUSDC =
         txContract && USDC_ADDRESSES.includes(txContract.toLowerCase());

       if (isUSDC) {
         if (isInbound) usdcChange += amount;
         if (isOutbound) usdcChange -= amount;
       } else if (tokenSymbol && tokenSymbol !== symbol) {
         // Base Token
         // If we already found a DIFFERENT base token in this tx, it's a multi-token trade (unsupported).
         // Mark as invalid/ignored
         tokenSymbol = 'INVALID';
       } else {
         tokenSymbol = symbol;
-        tokenAddress = tx.asset.contracts?.[0] || '';
+        tokenAddress = tx.asset.contracts?.[0] || tx.asset.contract || '';
         if (isInbound) tokenChange += amount;
         if (isOutbound) tokenChange -= amount;
       }
     });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c22bf3 and f2ce30b.

⛔ Files ignored due to path filters (2)
  • src/apps/pillarx-app/components/TokenMarketDataRow/tests/__snapshots__/LeftColumnTokenMarketDataRow.test.tsx.snap is excluded by !**/*.snap
  • src/apps/pillarx-app/components/TokensWithMarketDataTile/test/__snapshots__/TokensWithMarketDataTile.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • src/utils/pnl.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-09T12:40:15.629Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 391
File: src/apps/pulse/components/Sell/Sell.tsx:113-130
Timestamp: 2025-09-09T12:40:15.629Z
Learning: In the Pulse app Sell component, when a user changes/switches tokens, the input amount automatically resets to 0, which means liquidity validation state doesn't become stale when tokens change.

Applied to files:

  • src/utils/pnl.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (3)
src/utils/pnl.ts (3)

1-20: LGTM! Clean imports and USDC address extraction.

The imports are well-organized, and extracting USDC_ADDRESSES from allStableCurrencies provides a single source of truth, resolving the previous duplicate addresses issue.


118-200: LGTM! Solid WAC-based PnL calculation with proper edge case handling.

The function correctly implements weighted average cost (WAC) accounting, handles the zero/negative inventory guard (Line 133), prevents negative dust (Lines 147-148), and returns null when there are no buy transactions to avoid misleading +$0 displays.


378-548: LGTM! Comprehensive Relay-based PnL calculation with robust error handling.

This function demonstrates excellent practices:

  • Correctly processes both inTxs and outTxs (Line 406)
  • Proper timestamp normalization handling milliseconds vs seconds (Lines 411-415)
  • Case-insensitive USDC symbol checks (Lines 468, 480)
  • Comprehensive sanity checks preventing absurd values (Lines 506-531)
  • Clear fallback logic with warning logs

The reducer pattern for accumulating state changes is clean and efficient.

Copy link
Contributor

@vignesha22 vignesha22 left a comment

Choose a reason for hiding this comment

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

LGTM a lot of formatting from prettier on most of the files

@RanaBug RanaBug merged commit 757b1d7 into staging Dec 4, 2025
7 checks passed
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.

3 participants