Skip to content

Feat: yield balances migration#516

Open
petar-omni wants to merge 28 commits intomainfrom
feat/yield-balances-migration
Open

Feat: yield balances migration#516
petar-omni wants to merge 28 commits intomainfrom
feat/yield-balances-migration

Conversation

@petar-omni
Copy link
Copy Markdown
Contributor

@petar-omni petar-omni commented Apr 28, 2026

Summary by CodeRabbit

  • New Features

    • APY composition breakdown and a reward-rate breakdown component; personalized APY shown on positions
    • Yield API client and improved yield/validator fetching for more accurate opportunities and balances
    • Improved image fallback/monogram handling for token and provider icons
  • Bug Fixes

    • Various UI/text fixes across rewards, positions, and activity flows
  • Documentation

    • English and French translation updates for APY and reward labels
  • Chores

    • Tooling pinned/updated (Node/pnpm) and test additions for image and yield flows

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

🦋 Changeset detected

Latest commit: fda10e5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@stakekit/widget Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 28, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedopenapi-typescript@​7.13.09910010087100
Addedopenapi-fetch@​0.17.09910010088100
Addedopenapi-react-query@​0.5.41001008988100
Updated@​biomejs/​biome@​2.3.8 ⏵ 2.4.8100 +110010099 +1100

View full report

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 91c8b045-dff8-45c6-8835-3ae1c29f6543

📥 Commits

Reviewing files that changed from the base of the PR and between 2511e53 and fda10e5.

📒 Files selected for processing (1)
  • packages/widget/vite/vite.config.base.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/widget/vite/vite.config.base.ts

📝 Walkthrough

Walkthrough

Migration to a Yield API fetch client and domain model: adds a YieldApi client/provider, replaces many @stakekit/api-hooks DTOs with local domain types, changes API endpoints and hooks to use the new client, refactors components/pages/providers/tests, and updates toolchain (Node 24 / pnpm 10.33.2).

Changes

Cohort / File(s) Summary
Build & CI
/.github/workflows/ci.yml, mise.toml, package.json, packages/widget/.env.test
Pin Node 24 and pnpm 10.33.2, update biome devDependency, add VITE_YIELDS_API_URL/API_KEY for tests.
Docs & Metadata
AGENTS.md, .changeset/lemon-drinks-prove.md, .gitignore
Add agent guidance, release changeset for @stakekit/widget, and ignore opensrc/.
Yield API client & provider
packages/widget/src/providers/yield-api-client-provider/*
Add OpenAPI fetch client creator, provider component, hooks, actions wrappers, request helpers, types, and centralized response/error handling (geo-block, rich-error).
Private API surface
packages/widget/src/common/private-api.ts
Remove old yield-scan exports; add token/prices/balances/rewards/verification wrappers aligned to Yield API paths.
Domain types & helpers
packages/widget/src/domain/types/...
Introduce/replace many domain types (yields, actions, positions, reward-rate, pending-action, tokens, validators, addresses, errors, fees) and helpers to support yield mechanics and unified pending-action shapes.
Providers & wiring
packages/widget/src/providers/*, providers/yield-api-client-provider/*, providers/wagmi/index.ts
Add YieldApiClientProvider to provider tree; thread yieldApiFetchClient into enabled-networks, provider configs, wagmi builder, and other providers.
Components — images & icons
packages/widget/src/components/atoms/image/..., token-icon/..., removed image-fallback
Refactor Image to use inline SVG data-URL monogram fallback; rename props to wrapperProps/imgProps; broaden token types; remove ImageFallback component.
UI components & lists
components/molecules/reward-rate-breakdown, components/atoms/virtual-list, various molecule components
Add RewardRateBreakdown component; stop providing row measurement ref; update reward UI and token icon wiring across components.
Hooks — API & data changes
src/hooks/api/*, use-yield-validators.ts, use-multi-yields.ts, use-yield-balances-scan.ts, etc.
Switch many hooks to use YieldApiFetchClient and new endpoints; add paginated useYieldValidators; adapt query keys and request/response shapes.
Hooks — removals & wrappers
use-base-token, use-gas-fee-token, get-gas-mode-value (deleted)
Remove deprecated helpers; replace usages with domain helpers and mechanics fields.
Pages & flows
src/pages/** (earn, review, position-details, steps, activity, complete)
Refactor pages and flows to use Yield domain types, preview/create actions via Yield APIs, adjust state shapes, validators fetching, and translations; update machines to use yieldId and new tx endpoints.
Providers & stores
enter-stake-store, exit-stake-store, pending-action-store, wagmi, sk-wallet, etc.
Migrate store/init DTOs to Yield CreateAction/Manage shapes; add addresses to init data; thread yieldApiFetchClient; adjust error constructors and send/broadcast handling.
Utilities & formatters
src/utils/*
Adapt formatters/mappers to domain types; APToPercentage now uses shared formatNumber; gas/fee utilities read mechanics.gasFeeToken.
MSW & tests
src/worker.ts, tests/fixtures, tests/mocks, tests/use-cases/*, new tests
Expand MSW to mock yield-api endpoints (yields, validators, balances, actions, transactions); add fixtures and conversion helpers; update many test setups; add Image component tests and trust-incentive APY E2E tests.
Removed files
packages/widget/script.ts, src/common/get-gas-mode-value.ts, src/components/atoms/image-fallback/*
Delete obsolete script, helper, and ImageFallback component.
Translations
src/translation/English/translations.json, French/translations.json
Add APY composition, personalized APY labels, new balance-state translations and auto-sweep action labels.
Misc & formatting
packages/widget/public/mockServiceWorker.js, vite/vite.config.base.ts
Modernize mockServiceWorker JS style; disable screenshotFailures and reportCompressedSize in test/build config.

Sequence Diagram(s)

sequenceDiagram
    participant Widget as Widget App
    participant Provider as YieldApiClientProvider
    participant FetchClient as OpenAPI Fetch Client
    participant YieldAPI as Yield API Server
    participant DB as Backend

    rect rgba(200,150,255,0.5)
    Note over Widget,Provider: Client initialization
    Widget->>Provider: useYieldApiFetchClient()
    Provider->>Provider: createYieldApiFetchClient(apiKey, url, hooks)
    Provider-->>Widget: YieldApiFetchClient & OpenAPI client
    end

    rect rgba(150,200,255,0.5)
    Note over Widget,YieldAPI: Fetch yield opportunity
    Widget->>FetchClient: GET /v1/yields/{yieldId}
    FetchClient->>YieldAPI: HTTP GET
    YieldAPI->>DB: Resolve yield + mechanics
    DB-->>YieldAPI: YieldApiYieldDto
    YieldAPI-->>FetchClient: HTTP 200 + body
    FetchClient->>Widget: parsed data (geo-block / rich-error handled)
    end

    rect rgba(150,255,200,0.5)
    Note over Widget,YieldAPI: Create action (preview/submit)
    Widget->>FetchClient: POST /v1/actions/{enter|exit|manage} (YieldCreateActionDto)
    FetchClient->>YieldAPI: HTTP POST
    YieldAPI->>DB: Create action + build txs
    DB-->>YieldAPI: ActionDto + Transactions
    YieldAPI-->>FetchClient: Action response
    FetchClient-->>Widget: ActionDto for preview/submit
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through types and endpoints wide,
A Yield client tucked by my side,
SVG whiskers for tokens bright,
Legacy and new schemas now unite,
Tests hum—deployments in sight!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/yield-balances-migration

Copy link
Copy Markdown

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/widget/src/pages/details/earn-page/components/select-yield-section/use-animate-yield-percent.ts (1)

43-45: ⚠️ Potential issue | 🟠 Major

String placeholder path is ignored in final return.

On Line 43, when perReward is "- %", the function still returns estimatedRewards.extract()?.percentage, so the new placeholder may never be shown (and can become undefined after DTO migration).

💡 Proposed fix
-  return typeof perReward === "string" || config.env.isTestMode
-    ? estimatedRewards.extract()?.percentage
-    : transformedMotionValue;
+  return typeof perReward === "string"
+    ? perReward
+    : config.env.isTestMode
+      ? estimatedRewards.extract()?.percentage
+      : transformedMotionValue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/widget/src/pages/details/earn-page/components/select-yield-section/use-animate-yield-percent.ts`
around lines 43 - 45, The current return expression returns
estimatedRewards.extract()?.percentage when perReward is a string placeholder
like "- %", hiding the placeholder and possibly yielding undefined; change the
logic in use-animate-yield-percent so that if typeof perReward === "string" you
return perReward directly (not estimatedRewards.extract()?.percentage),
otherwise preserve the existing test-mode and motion-value behavior; also guard
the estimatedRewards.extract()?.percentage with a nullish fallback (e.g., ??
perReward or a safe default) to avoid returning undefined after DTO migration.
packages/widget/tests/use-cases/select-opportunity.test.tsx (1)

117-129: ⚠️ Potential issue | 🟡 Minor

Replace wildcard matcher with legacyApiRoute() for /v1/tokens endpoint.

This endpoint is the legacy API version used for token retrieval (per use-default-tokens.ts). Using the route helper maintains consistency with other endpoint mocking in this test (lines 65, 135-141) and prevents potential host/base-URL wiring issues.

Suggested change
-      http.get("*/v1/tokens", async () => {
+      http.get(legacyApiRoute("/v1/tokens"), async () => {
         await delay();
         return HttpResponse.json([
           {
             token,
             availableYields: [
               "ethereum-eth-lido-staking",
               "ethereum-eth-stakewise-staking",
               "ethereum-eth-reth-staking",
             ],
           },
         ]);
       }),

Note: This pattern appears in multiple test setup files (external-provider, deep-links-flow, staking-flow, gas-warning-flow, etc.). Consider applying the same fix across the test suite for consistency.

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

In `@packages/widget/tests/use-cases/select-opportunity.test.tsx` around lines 117
- 129, Replace the wildcard http.get matcher for the "/v1/tokens" endpoint with
the helper legacyApiRoute() so the test uses the same legacy API route as
use-default-tokens.ts; locate the http.get(...) call that returns
HttpResponse.json([...]) and change its first argument to
legacyApiRoute("/v1/tokens") to avoid host/base-URL wiring issues and match
other mocks in this suite.
packages/widget/src/providers/sk-wallet/errors.ts (1)

30-30: ⚠️ Potential issue | 🟡 Minor

Remove dead code: unused SendTransactionError instantiation.

This line creates an error instance that's never used or assigned. It appears to be leftover debug/test code.

🗑️ Proposed fix
-
-new SendTransactionError();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/providers/sk-wallet/errors.ts` at line 30, The line
instantiating SendTransactionError is dead code and should be removed: delete
the standalone `new SendTransactionError();` expression in the errors module so
the class is only defined/used where intended; ensure no other code relies on
that unused instantiation and run tests/typechecks to confirm no references to
the unused instance remain (look for the SendTransactionError symbol in the same
file or nearby exports).
packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts (1)

334-337: ⚠️ Potential issue | 🟠 Major

assign call has no effect in error handler.

On line 335, assign(({ context }) => ({ ...context, error })) is called but its return value is not used. In XState, assign returns an action object that needs to be executed as part of the machine's actions array, not called imperatively. The error won't be stored in context.

🐛 Proposed fix

The error assignment should be handled via the __SUBMIT_ERROR__ event's actions in the machine definition (around line 265), or the error should be passed with the event:

               .caseOf({
                   Right() {
                     self.send({ type: "__SUBMIT_SUCCESS__" });
                   },
                   Left(error) {
-                    assign(({ context }) => ({ ...context, error }));
-                    self.send({ type: "__SUBMIT_ERROR__" });
+                    self.send({ type: "__SUBMIT_ERROR__", error });
                   },
                 }),

Then update the event type and add an action handler:

-        | { type: "__SUBMIT_ERROR__" },
+        | { type: "__SUBMIT_ERROR__"; error?: Error },

And in the on handler for __SUBMIT_ERROR__:

           __SUBMIT_ERROR__: {
             target: ".error",
+            actions: assign(({ event }) => ({
+              error: Maybe.fromNullable(event.error),
+            })),
           },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts`
around lines 334 - 337, The assign call inside Left is not applied because
assign returns an action object instead of mutating context; update Left to
include the error in the event payload (send an event with type
"__SUBMIT_ERROR__" and the error) and then move the assign logic into the
machine's event handler for "__SUBMIT_ERROR__" (add an action that assigns error
into context via assign in the machine definition near the existing
__SUBMIT_ERROR__ handling), or alternatively replace the send call with
executing the assign action directly as part of the actions array; reference:
Left, assign, self.send, __SUBMIT_ERROR__, and the machine definition where
__SUBMIT_ERROR__ actions are declared.
🟡 Minor comments (10)
packages/widget/src/components/molecules/amount-toggle/index.tsx-62-62 (1)

62-62: ⚠️ Potential issue | 🟡 Minor

Remove or justify this unrelated export reordering.

The exports were reordered to alphabetical order (Amount before Root), but no ESLint or Prettier configuration enforces this convention. This change appears unrelated to the PR's stated objective of yield balances migration and lacks explanation. Either revert this change or clarify its purpose.

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

In `@packages/widget/src/components/molecules/amount-toggle/index.tsx` at line 62,
The change reordered the named exports to "Amount, Root" which is unrelated to
the PR; revert the export ordering to the original sequence (export { Root,
Amount }) in the amount-toggle module or add a short inline comment explaining
why alphabetical order is required and reference a style rule, or add a
lint/prettier rule that enforces alphabetical exports; update the export
statement for Amount and Root accordingly (symbols: Amount, Root) so the diff
only contains intentional changes.
packages/widget/.env.test-1-4 (1)

1-4: ⚠️ Potential issue | 🟡 Minor

Reorder keys to clear dotenv-linter warnings.

Current ordering triggers static analysis warnings; reordering keeps CI/lint output clean.

Suggested reorder
-VITE_API_URL=https://api.stakek.it/
-VITE_YIELDS_API_URL=https://api.yield.xyz/
-VITE_API_KEY=vitest-api-key
 MODE=test
+VITE_API_KEY=vitest-api-key
+VITE_API_URL=https://api.stakek.it/
+VITE_YIELDS_API_URL=https://api.yield.xyz/
Based on learnings: Confirm nothing is broken with lint command which checks lint/type errors after making changes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/.env.test` around lines 1 - 4, Reorder the environment
variable keys in the packages/widget/.env.test file to satisfy dotenv-linter
(e.g., sort keys alphabetically or follow the project's expected key order such
that VITE_API_KEY, VITE_API_URL, VITE_YIELDS_API_URL, MODE are in the corrected
order); update the .env.test file accordingly and then run the project's lint
command (the same lint/type-check step used in CI) to confirm no lint or type
errors were introduced.
packages/widget/src/pages-dashboard/activity/activity-details.page.tsx-89-89 (1)

89-89: ⚠️ Potential issue | 🟡 Minor

Confirm that integrationId prop receives and semantically represents yield identifiers.

The change is functionally correct: selectedAction.yieldId is passed to integrationId, which is then used as a yield identifier in isEthenaUsdeStaking() (compares against "ethena-usde-staking") and useYieldOpportunity(). However, the prop naming creates a semantic inconsistency—the parameter is named integrationId but receives and is treated as a yield ID throughout downstream code. This works because both represent the same string identifier in this codebase, but the naming should be clarified or unified for maintainability.

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

In `@packages/widget/src/pages-dashboard/activity/activity-details.page.tsx` at
line 89, The prop integrationId is being passed selectedAction.yieldId but is
treated as a yield identifier downstream (see isEthenaUsdeStaking() and
useYieldOpportunity()), causing a semantic mismatch; update the component API so
the prop name matches its meaning: either rename the prop integrationId to
yieldId where it’s declared/consumed or change the call site to pass
selectedAction.yieldId into a yieldId prop, and update all usages of
integrationId inside the component and in calls to isEthenaUsdeStaking() and
useYieldOpportunity() to use yieldId so naming is unified and self-explanatory.
packages/widget/src/translation/French/translations.json-163-163 (1)

163-163: ⚠️ Potential issue | 🟡 Minor

Fix French typography in APY composition label.

Line 163 should use Jusqu’à {{value}} (with apostrophe/accent), not Jusqu'a {{value}}.

✏️ Proposed fix
-      "up_to": "Jusqu'a {{value}}"
+      "up_to": "Jusqu’à {{value}}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/translation/French/translations.json` at line 163, Update
the French translation value for the "up_to" key in translations.json to use the
correct typographic apostrophe and accented "à": replace "Jusqu'a {{value}}"
with "Jusqu’à {{value}}" (use the proper right single quotation mark + "à") so
the label reads correctly in French.
packages/widget/src/translation/French/translations.json-407-407 (1)

407-407: ⚠️ Potential issue | 🟡 Minor

Fix accent in personalized APY label.

Line 407 should be APY personnalisé.

✏️ Proposed fix
-    "personalized_apy": "APY personnalise",
+    "personalized_apy": "APY personnalisé",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/translation/French/translations.json` at line 407, Update
the French translation value for the key "personalized_apy" in translations.json
to use the correct accent: change the string "APY personnalise" to "APY
personnalisé" so the label displays the proper French accent.
packages/widget/src/hooks/api/use-tokens-prices.ts-18-27 (1)

18-27: ⚠️ Potential issue | 🟡 Minor

Fix duplicate token in tokenList: second entry should reference yield's base token.

The comment on line 9 documents "Requested Token + Yield base token + Yield gas fee token", but line 23 incorrectly uses val.token twice:

tokenList: [val.token, val.token, val.yieldDto.mechanics.gasFeeToken]

The second entry should be the yield's base token, not the requested token:

tokenList: [val.token, val.yieldDto.token, val.yieldDto.mechanics.gasFeeToken]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/hooks/api/use-tokens-prices.ts` around lines 18 - 27, The
tokenList currently contains a duplicate requested token in the priceRequestDto
creation; update the mapping inside the useMemo (where Maybe.fromRecord({
yieldDto, token }) is mapped to the DTO) so tokenList becomes [val.token,
val.yieldDto.token, val.yieldDto.mechanics.gasFeeToken] (i.e., replace the
second val.token with the yield's base token val.yieldDto.token) to match the
documented "Requested Token + Yield base token + Yield gas fee token".
packages/widget/src/hooks/use-positions-data.ts-76-80 (1)

76-80: ⚠️ Potential issue | 🟡 Minor

Fix validator-address fallback when validators is an empty array.

Current ?? fallback skips balance.validator whenever balance.validators is present but empty. That can drop addresses unexpectedly.

Suggested fix
-const getBalanceValidatorAddresses = (balance: YieldBalanceDto) =>
-  (
-    balance.validators?.map((validator) => validator.address) ??
-    (balance.validator?.address ? [balance.validator.address] : [])
-  ).filter(Boolean);
+const getBalanceValidatorAddresses = (balance: YieldBalanceDto) => {
+  const fromValidators = (
+    balance.validators?.map((validator) => validator.address) ?? []
+  ).filter(Boolean);
+
+  if (fromValidators.length > 0) return fromValidators;
+  return balance.validator?.address ? [balance.validator.address] : [];
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/hooks/use-positions-data.ts` around lines 76 - 80,
getBalanceValidatorAddresses uses the nullish coalescing operator (??) which
treats an empty validators array as present and therefore skips the fallback to
balance.validator; change the expression to check for a non-empty validators
array (e.g., use a length check or logical OR) so that when balance.validators
is an empty array it falls back to balance.validator.address; update the
function getBalanceValidatorAddresses to use something like (balance.validators
&& balance.validators.length ? balance.validators.map(v => v.address) :
(balance.validator?.address ? [balance.validator.address] : [])) and keep the
final .filter(Boolean) intact.
packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts-46-53 (1)

46-53: ⚠️ Potential issue | 🟡 Minor

Potential issue: providerId may be included when undefined.

When b.validator?.providerId is truthy (line 48), the returned object includes both providerId and validatorAddress. However, if b.validator.address is undefined, you'll be including validatorAddress: undefined in the DTO, which may cause issues downstream.

🛡️ Suggested defensive fix
             return List.find(
               (b) => !!b.validator?.providerId,
               val.stakedOrLiquidBalances
             ).map((b) => ({
               providerId: b.validator?.providerId,
-              validatorAddress: b.validator?.address,
+              ...(b.validator?.address && { validatorAddress: b.validator.address }),
             }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts`
around lines 46 - 53, The code in use-stake-exit-request-dto uses List.find on
val.stakedOrLiquidBalances when
isYieldIntegrationAggregator(val.integrationData) is true but may return an
object with validatorAddress undefined; change the mapping logic so you only
include providerId/validatorAddress when validator.address is defined (e.g.,
check b.validator?.address before returning the DTO), or return undefined/null
instead of an object with validatorAddress: undefined; update the mapping in the
List.find result to guard on b.validator?.address so downstream consumers never
receive validatorAddress set to undefined.
packages/widget/src/providers/yield-api-client-provider/index.tsx-84-91 (1)

84-91: ⚠️ Potential issue | 🟡 Minor

Potential excessive client recreation due to i18n dependency.

The i18n object from useTranslation() may change reference on every render or language change. Since createYieldApiFetchClient is called inside useMemo with i18n as a dependency, this could cause the fetch client to be recreated more often than necessary. Consider using a ref or extracting only the stable parts of i18n needed for error handling.

♻️ Suggested fix using a ref for stable i18n reference
 export const YieldApiClientProvider = ({ children }: PropsWithChildren) => {
   const { apiKey, yieldsApiUrl } = useSettings();
   const { i18n } = useTranslation();
   const url = yieldsApiUrl ?? config.env.yieldsApiUrl;
+  const i18nRef = useRef(i18n);
+  i18nRef.current = i18n;
 
   const clients = useMemo(() => {
-    const fetchClient = createYieldApiFetchClient({ apiKey, i18n, url });
+    const fetchClient = createYieldApiFetchClient({ 
+      apiKey, 
+      i18n: i18nRef.current, 
+      url 
+    });
 
     return {
       fetchClient,
       queryClient: createClient(fetchClient),
     };
-  }, [apiKey, i18n, url]);
+  }, [apiKey, url]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/providers/yield-api-client-provider/index.tsx` around
lines 84 - 91, The useMemo block recreates fetchClient whenever the entire i18n
object reference changes; update createYieldApiFetchClient usage in the useMemo
so it depends only on stable values (e.g., i18n.language or a stable translator
function) or keep i18n in a ref and read from that ref inside error
handlers—specifically, modify the useMemo that calls createYieldApiFetchClient
and createClient so its dependency array uses apiKey, url and a stable i18n
token (or remove i18n and use an i18nRef where i18nRef.current = i18n) to avoid
recreating fetchClient unnecessarily.
packages/widget/src/pages/position-details/hooks/use-position-details.ts-192-200 (1)

192-200: ⚠️ Potential issue | 🟡 Minor

Potential division by zero in share conversion calculation.

While there's a filter for yb.shareAmount && yb.amount (line 192), if yb.amount is "0" (a truthy string), the division on line 197-199 would result in Infinity. Consider adding a numeric check.

🛡️ Proposed fix to guard against zero division
             .filter((yb) => yb.shareAmount && yb.amount && !yb.token.isPoints)
+            .filter((yb) => new BigNumber(yb.amount ?? 0).isGreaterThan(0))
             .forEach((yb) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/pages/position-details/hooks/use-position-details.ts`
around lines 192 - 200, The division can produce Infinity when yb.amount is a
non-empty "0" string; update the conversion logic in use-position-details (the
block that filters over yb and calls acc.set with defaultFormattedNumber(new
BigNumber(yb.shareAmount).dividedBy(new BigNumber(yb.amount)))) to coerce
yb.amount to a numeric value and guard against zero before dividing (e.g.,
parse/convert yb.amount to a Number or BigNumber and skip or set a safe fallback
when it equals 0), ensuring the value passed to defaultFormattedNumber is valid;
keep references to yb.shareAmount, yb.amount, defaultFormattedNumber, BigNumber,
acc.set, and yb.token.symbol/yb.shareToken?.symbol when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae3c0135-c881-4a62-96ea-2a41e0fe728e

📥 Commits

Reviewing files that changed from the base of the PR and between 0b60e3e and dfbc459.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (223)
  • .changeset/lemon-drinks-prove.md
  • .github/workflows/ci.yml
  • .gitignore
  • AGENTS.md
  • mise.toml
  • package.json
  • packages/widget/.env.test
  • packages/widget/package.json
  • packages/widget/public/mockServiceWorker.js
  • packages/widget/script.ts
  • packages/widget/src/common/check-gas-amount.ts
  • packages/widget/src/common/delay-api-requests.ts
  • packages/widget/src/common/get-gas-mode-value.ts
  • packages/widget/src/common/private-api.ts
  • packages/widget/src/components/atoms/image-fallback/index.tsx
  • packages/widget/src/components/atoms/image-fallback/styles.css.ts
  • packages/widget/src/components/atoms/image/index.tsx
  • packages/widget/src/components/atoms/token-icon/index.tsx
  • packages/widget/src/components/atoms/token-icon/network-icon-image/index.tsx
  • packages/widget/src/components/atoms/token-icon/provider-icon/index.tsx
  • packages/widget/src/components/atoms/token-icon/token-icon-container/hooks/use-variant-token-urls.ts
  • packages/widget/src/components/atoms/token-icon/token-icon-container/index.tsx
  • packages/widget/src/components/atoms/token-icon/token-icon-image/index.tsx
  • packages/widget/src/components/atoms/virtual-list/index.tsx
  • packages/widget/src/components/molecules/amount-toggle/index.tsx
  • packages/widget/src/components/molecules/reward-rate-breakdown/index.tsx
  • packages/widget/src/components/molecules/reward-token-details/index.tsx
  • packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx
  • packages/widget/src/components/molecules/select-validator/index.tsx
  • packages/widget/src/components/molecules/select-validator/meta-info.tsx
  • packages/widget/src/components/molecules/select-validator/select-validator-list.tsx
  • packages/widget/src/components/molecules/select-yield/index.tsx
  • packages/widget/src/config/index.ts
  • packages/widget/src/domain/index.ts
  • packages/widget/src/domain/types/action.ts
  • packages/widget/src/domain/types/addresses.ts
  • packages/widget/src/domain/types/errors.ts
  • packages/widget/src/domain/types/fees.ts
  • packages/widget/src/domain/types/init-params.ts
  • packages/widget/src/domain/types/pending-action.ts
  • packages/widget/src/domain/types/positions.ts
  • packages/widget/src/domain/types/price.ts
  • packages/widget/src/domain/types/reward-rate.ts
  • packages/widget/src/domain/types/rewards.ts
  • packages/widget/src/domain/types/settings.ts
  • packages/widget/src/domain/types/stake.ts
  • packages/widget/src/domain/types/token-balance.ts
  • packages/widget/src/domain/types/tokens.ts
  • packages/widget/src/domain/types/transaction.ts
  • packages/widget/src/domain/types/tron.ts
  • packages/widget/src/domain/types/validators.ts
  • packages/widget/src/domain/types/wallets/generic-wallet.ts
  • packages/widget/src/domain/types/yield-api.ts
  • packages/widget/src/domain/types/yields.ts
  • packages/widget/src/hooks/api/use-activity-actions.ts
  • packages/widget/src/hooks/api/use-default-tokens.ts
  • packages/widget/src/hooks/api/use-multi-yields.ts
  • packages/widget/src/hooks/api/use-prices.ts
  • packages/widget/src/hooks/api/use-token-balances-scan.ts
  • packages/widget/src/hooks/api/use-tokens-prices.ts
  • packages/widget/src/hooks/api/use-yield-balances-scan.ts
  • packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.ts
  • packages/widget/src/hooks/api/use-yield-opportunity/index.ts
  • packages/widget/src/hooks/api/use-yield-validators.ts
  • packages/widget/src/hooks/navigation/use-dashboard-tabs-page-match.ts
  • packages/widget/src/hooks/navigation/use-unstake-or-pending-action-params.ts
  • packages/widget/src/hooks/use-base-token.ts
  • packages/widget/src/hooks/use-estimated-rewards.ts
  • packages/widget/src/hooks/use-force-max-amount.ts
  • packages/widget/src/hooks/use-gas-fee-token.ts
  • packages/widget/src/hooks/use-gas-warning-check.ts
  • packages/widget/src/hooks/use-geo-block.ts
  • packages/widget/src/hooks/use-handle-deep-links.ts
  • packages/widget/src/hooks/use-init-params.ts
  • packages/widget/src/hooks/use-init-query-params.ts
  • packages/widget/src/hooks/use-max-min-yield-amount.ts
  • packages/widget/src/hooks/use-position-balance-by-type.ts
  • packages/widget/src/hooks/use-position-balances.ts
  • packages/widget/src/hooks/use-positions-data.ts
  • packages/widget/src/hooks/use-provider-details.ts
  • packages/widget/src/hooks/use-reward-token-details/get-reward-token-symbols.tsx
  • packages/widget/src/hooks/use-reward-token-details/index.ts
  • packages/widget/src/hooks/use-rewards-summary.ts
  • packages/widget/src/hooks/use-rich-errors.ts
  • packages/widget/src/hooks/use-staked-or-liquid-balance.ts
  • packages/widget/src/hooks/use-summary.tsx
  • packages/widget/src/hooks/use-under-maintenance.ts
  • packages/widget/src/hooks/use-yield-meta-info.tsx
  • packages/widget/src/hooks/use-yield-type.ts
  • packages/widget/src/pages-dashboard/activity/action-list-item/index.tsx
  • packages/widget/src/pages-dashboard/activity/activity-details.page.tsx
  • packages/widget/src/pages-dashboard/activity/activity.page.tsx
  • packages/widget/src/pages-dashboard/activity/position-balances.tsx
  • packages/widget/src/pages-dashboard/overview/earn-page/utila-select-validator-section.tsx
  • packages/widget/src/pages-dashboard/overview/earn-page/utila-select-validator-trigger.tsx
  • packages/widget/src/pages-dashboard/overview/positions/components/positions-list-item.tsx
  • packages/widget/src/pages-dashboard/overview/positions/hooks/use-position-list-item.ts
  • packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx
  • packages/widget/src/pages-dashboard/position-details/components/position-details-info.tsx
  • packages/widget/src/pages-dashboard/position-details/components/provider-details.tsx
  • packages/widget/src/pages-dashboard/position-details/components/top-header.tsx
  • packages/widget/src/pages-dashboard/rewards/components/positions-list-item.tsx
  • packages/widget/src/pages/complete/hooks/use-activity-complete.hook.ts
  • packages/widget/src/pages/complete/hooks/use-complete.hook.ts
  • packages/widget/src/pages/complete/pages/activity-complete.page.tsx
  • packages/widget/src/pages/complete/pages/common.page.tsx
  • packages/widget/src/pages/complete/pages/pending-complete.page.tsx
  • packages/widget/src/pages/complete/pages/stake-complete.page.tsx
  • packages/widget/src/pages/complete/pages/unstake-complete.page.tsx
  • packages/widget/src/pages/complete/state/index.tsx
  • packages/widget/src/pages/components/meta-info/index.tsx
  • packages/widget/src/pages/details/activity-page/components/action-list-item/index.tsx
  • packages/widget/src/pages/details/activity-page/components/list-item-bullet/index.tsx
  • packages/widget/src/pages/details/activity-page/hooks/use-action-list-item.ts
  • packages/widget/src/pages/details/activity-page/hooks/use-activity-page.tsx
  • packages/widget/src/pages/details/activity-page/state/activity-page.context.tsx
  • packages/widget/src/pages/details/activity-page/types.ts
  • packages/widget/src/pages/details/earn-page/components/extra-args-selection/index.tsx
  • packages/widget/src/pages/details/earn-page/components/select-provider/index.tsx
  • packages/widget/src/pages/details/earn-page/components/select-token-section/index.tsx
  • packages/widget/src/pages/details/earn-page/components/select-token-section/select-token-list-item.tsx
  • packages/widget/src/pages/details/earn-page/components/select-validator-section/index.tsx
  • packages/widget/src/pages/details/earn-page/components/select-validator-section/select-validator-trigger.tsx
  • packages/widget/src/pages/details/earn-page/components/select-validator-section/use-select-validator.ts
  • packages/widget/src/pages/details/earn-page/components/select-yield-section/index.tsx
  • packages/widget/src/pages/details/earn-page/components/select-yield-section/select-opportunity.tsx
  • packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx
  • packages/widget/src/pages/details/earn-page/components/select-yield-section/staked-via.tsx
  • packages/widget/src/pages/details/earn-page/components/select-yield-section/use-animate-yield-percent.ts
  • packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx
  • packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx
  • packages/widget/src/pages/details/earn-page/state/types.ts
  • packages/widget/src/pages/details/earn-page/state/use-get-init-yield.ts
  • packages/widget/src/pages/details/earn-page/state/use-get-token-balances-map.ts
  • packages/widget/src/pages/details/earn-page/state/use-init-token.ts
  • packages/widget/src/pages/details/earn-page/state/use-init-yield.ts
  • packages/widget/src/pages/details/earn-page/state/use-pending-action-deep-link.ts
  • packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts
  • packages/widget/src/pages/details/earn-page/state/use-token-balance.ts
  • packages/widget/src/pages/details/earn-page/state/use-track-state-events.ts
  • packages/widget/src/pages/details/earn-page/state/utils.ts
  • packages/widget/src/pages/details/earn-page/types.ts
  • packages/widget/src/pages/details/positions-page/components/positions-list-item.tsx
  • packages/widget/src/pages/details/positions-page/hooks/use-position-list-item.ts
  • packages/widget/src/pages/details/positions-page/hooks/use-positions.ts
  • packages/widget/src/pages/position-details/components/amount-block.tsx
  • packages/widget/src/pages/position-details/components/position-balances.tsx
  • packages/widget/src/pages/position-details/components/provider-details.tsx
  • packages/widget/src/pages/position-details/components/static-action-block.tsx
  • packages/widget/src/pages/position-details/hooks/use-pending-actions.ts
  • packages/widget/src/pages/position-details/hooks/use-position-details.ts
  • packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts
  • packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts
  • packages/widget/src/pages/position-details/hooks/use-validator-addresses-handling.ts
  • packages/widget/src/pages/position-details/hooks/utils.ts
  • packages/widget/src/pages/position-details/position-details.page.tsx
  • packages/widget/src/pages/position-details/state/index.tsx
  • packages/widget/src/pages/position-details/state/types.ts
  • packages/widget/src/pages/position-details/state/utils.ts
  • packages/widget/src/pages/review/hooks/use-action-review.hook.ts
  • packages/widget/src/pages/review/hooks/use-fees.ts
  • packages/widget/src/pages/review/hooks/use-pending-review.hook.ts
  • packages/widget/src/pages/review/hooks/use-stake-review.hook.ts
  • packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts
  • packages/widget/src/pages/review/pages/action-review.page.tsx
  • packages/widget/src/pages/review/pages/common-page/common.page.tsx
  • packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx
  • packages/widget/src/pages/review/pages/pending-review.page.tsx
  • packages/widget/src/pages/review/pages/unstake-review.page.tsx
  • packages/widget/src/pages/steps/hooks/use-steps-machine.hook.ts
  • packages/widget/src/pages/steps/hooks/use-steps.hook.ts
  • packages/widget/src/pages/steps/pages/activity-steps.page.tsx
  • packages/widget/src/pages/steps/pages/common.page.tsx
  • packages/widget/src/pages/steps/pages/pending-steps.page.tsx
  • packages/widget/src/pages/steps/pages/stake-steps.page.tsx
  • packages/widget/src/pages/steps/pages/tx-state.tsx
  • packages/widget/src/pages/steps/pages/unstake-steps.page.tsx
  • packages/widget/src/providers/activity-provider/index.tsx
  • packages/widget/src/providers/api/get-enabled-networks.ts
  • packages/widget/src/providers/cosmos/config.ts
  • packages/widget/src/providers/enter-stake-store/index.tsx
  • packages/widget/src/providers/ethereum/config.ts
  • packages/widget/src/providers/exit-stake-store/index.tsx
  • packages/widget/src/providers/index.tsx
  • packages/widget/src/providers/misc/solana-connector.ts
  • packages/widget/src/providers/pending-action-store/index.tsx
  • packages/widget/src/providers/settings/types.ts
  • packages/widget/src/providers/sk-wallet/errors.ts
  • packages/widget/src/providers/sk-wallet/index.tsx
  • packages/widget/src/providers/sk-wallet/use-additional-addresses.ts
  • packages/widget/src/providers/substrate/config.ts
  • packages/widget/src/providers/wagmi/index.ts
  • packages/widget/src/providers/yield-api-client-provider/actions.ts
  • packages/widget/src/providers/yield-api-client-provider/index.tsx
  • packages/widget/src/providers/yield-api-client-provider/request-helpers.ts
  • packages/widget/src/providers/yield-api-client-provider/types.ts
  • packages/widget/src/translation/English/translations.json
  • packages/widget/src/translation/French/translations.json
  • packages/widget/src/types/yield-api-schema.d.ts
  • packages/widget/src/utils/formatters.ts
  • packages/widget/src/utils/index.ts
  • packages/widget/src/utils/mappers.ts
  • packages/widget/src/utils/region-iso-3166-codes.ts
  • packages/widget/src/worker.ts
  • packages/widget/tests/components/atoms/image.test.tsx
  • packages/widget/tests/components/atoms/token-icon-image.test.tsx
  • packages/widget/tests/fixtures/index.ts
  • packages/widget/tests/mocks/api-routes.ts
  • packages/widget/tests/use-cases/deep-links-flow/param-validation.test.tsx
  • packages/widget/tests/use-cases/deep-links-flow/setup.ts
  • packages/widget/tests/use-cases/external-provider/setup.ts
  • packages/widget/tests/use-cases/gas-warning-flow/gas-warning-flow.test.tsx
  • packages/widget/tests/use-cases/gas-warning-flow/setup.ts
  • packages/widget/tests/use-cases/geo-block.test.tsx
  • packages/widget/tests/use-cases/renders-initial-page.test.tsx
  • packages/widget/tests/use-cases/select-opportunity.test.tsx
  • packages/widget/tests/use-cases/sk-wallet.test.tsx
  • packages/widget/tests/use-cases/staking-flow/setup.ts
  • packages/widget/tests/use-cases/staking-flow/staking-flow.test.tsx
  • packages/widget/tests/use-cases/trust-incentive-apy/setup.ts
  • packages/widget/tests/use-cases/trust-incentive-apy/trust-incentive-apy.test.tsx
  • packages/widget/tests/use-cases/under-maintenance.test.tsx
  • packages/widget/vite/vite.config.base.ts
💤 Files with no reviewable changes (7)
  • packages/widget/src/hooks/navigation/use-dashboard-tabs-page-match.ts
  • packages/widget/src/components/atoms/image-fallback/styles.css.ts
  • packages/widget/src/hooks/use-base-token.ts
  • packages/widget/src/components/atoms/image-fallback/index.tsx
  • packages/widget/src/common/get-gas-mode-value.ts
  • packages/widget/src/hooks/use-gas-fee-token.ts
  • packages/widget/script.ts

Comment on lines +39 to +43
<Box
{...imgProps}
src={src ?? generatedFallbackSrc}
as="img"
onError={onError}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat empty src values as missing.

Line 41 only falls back for null/undefined. If a caller passes "" for a missing logo, this will render a broken image instead of the monogram fallback.

Suggested fix
-        src={src ?? generatedFallbackSrc}
+        src={src?.trim() ? src : generatedFallbackSrc}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/components/atoms/image/index.tsx` around lines 39 - 43,
The src fallback only handles null/undefined but not empty strings, so update
the image props in the Box component (the src prop used with
generatedFallbackSrc) to treat empty or whitespace-only strings as missing;
replace the nullish fallback with a check like "use src if it is a non-empty
string (trimmed), otherwise use generatedFallbackSrc" so the monogram fallback
renders when callers pass "" or whitespace for src (also keep onError and
existing imgProps unchanged).

Comment on lines 39 to 43
const tokenMappingResult = Maybe.fromNullable(tokenIconMapping)
.chainNullable((mapping) => {
if (typeof mapping === "function") {
return mapping(token);
return mapping(token as Parameters<typeof mapping>[0]);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tokenIconMapping type and settings contract =="
rg -n -C4 '\btokenIconMapping\b' packages/widget/src

echo
echo "== TokenDto / YieldTokenDto definitions =="
fd 'tokens.ts' packages/widget/src/domain/types -x sed -n '1,240p' {}

echo
echo "== Hook implementation =="
fd 'use-variant-token-urls.ts' packages/widget/src -x sed -n '1,140p' {}

Repository: stakekit/widget

Length of output: 7550


Update the settings contract to reflect that tokenIconMapping callbacks must handle both TokenDto and YieldTokenDto.

The cast at Line 42 (mapping(token as Parameters<typeof mapping>[0])) hides a type mismatch: the settings type defines tokenIconMapping callbacks to accept only TokenDto, but the hook now accepts TokenDto | YieldTokenDto. Integrators' callbacks written before YieldTokenDto support may fail at runtime when passed a yield token. Either widen the callback type in the settings contract to (token: TokenDto | YieldTokenDto) => string, or normalize the token shape before calling the callback.

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

In
`@packages/widget/src/components/atoms/token-icon/token-icon-container/hooks/use-variant-token-urls.ts`
around lines 39 - 43, The tokenIconMapping callback is currently typed to accept
only TokenDto but use-variant-token-urls now passes TokenDto | YieldTokenDto
(see Maybe.fromNullable(...).chainNullable and the mapping(token as
Parameters<typeof mapping>[0]) cast); remove this unsafe cast by either widening
the settings contract so tokenIconMapping is declared as (token: TokenDto |
YieldTokenDto) => string, or normalize/convert a YieldTokenDto to the TokenDto
shape inside useVariantTokenUrls before calling mapping; update the
tokenIconMapping type in the settings contract and any call sites (or implement
a small normalizer in useVariantTokenUrls) so callbacks always receive the
declared token shape without runtime type mismatches.

Comment on lines 17 to +19
<Image
data-rk="token-logo"
containerProps={{ hw: tokenLogoHw, "data-rk": "token-logo" }}
wrapperProps={{ hw: tokenLogoHw, "data-rk": "token-logo" }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find packages/widget/src/components/atoms -name "*.tsx" -o -name "*.ts" | grep -E "(token-icon-image|image)" | head -20

Repository: stakekit/widget

Length of output: 344


🏁 Script executed:

cat -n packages/widget/src/components/atoms/token-icon/token-icon-image/index.tsx

Repository: stakekit/widget

Length of output: 757


🏁 Script executed:

cat -n packages/widget/src/components/atoms/image/index.tsx

Repository: stakekit/widget

Length of output: 2347


🏁 Script executed:

rg "Image" packages/widget/src/components/atoms -A 5 -B 1 | grep -E "(data-rk|Image|wrapperProps)" | head -40

Repository: stakekit/widget

Length of output: 3604


🏁 Script executed:

fd "tsconfig" packages/widget | head -5

Repository: stakekit/widget

Length of output: 125


🏁 Script executed:

cat packages/widget/tsconfig.json | head -30

Repository: stakekit/widget

Length of output: 276


🏁 Script executed:

cat /dev/null | head -0 && cat ../../tsconfig.json | grep -E "(strict|jsx)" | head -10

Repository: stakekit/widget

Length of output: 111


🏁 Script executed:

rg "data-rk" packages/widget/src/components/atoms/token-icon-image -A 3 -B 3

Repository: stakekit/widget

Length of output: 153


🏁 Script executed:

cat tsconfig.json | grep -E "(strict|jsx)"

Repository: stakekit/widget

Length of output: 103


🏁 Script executed:

rg "data-rk" packages/widget/src/components/atoms/token-icon -A 2 -B 2

Repository: stakekit/widget

Length of output: 1247


🏁 Script executed:

rg "export const Image" packages/widget/src/components/atoms/image -A 20 | head -30

Repository: stakekit/widget

Length of output: 1613


🏁 Script executed:

# Check if there are any component usages with extraneous props on Image
rg "<Image" packages/widget/src -A 5 | grep -E "(<Image|data-rk)" | head -20

Repository: stakekit/widget

Length of output: 1570


Remove the extraneous data-rk prop from <Image>.

The prop on line 18 is not part of ImageProps and will fail strict TypeScript type-checking. The same attribute is already defined in wrapperProps on line 19.

Suggested fix
   <Image
-    data-rk="token-logo"
     wrapperProps={{ hw: tokenLogoHw, "data-rk": "token-logo" }}
     imgProps={{ hw: tokenLogoHw }}
     src={mainUrl ?? fallbackUrl}
     fallbackName={name}
   />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/components/atoms/token-icon/token-icon-image/index.tsx`
around lines 17 - 19, Remove the extraneous top-level prop named `data-rk`
passed to the `Image` component (the `Image` JSX in token-icon-image/index.tsx);
it is not part of `ImageProps` and duplicates the same `data-rk` already present
in `wrapperProps`. Edit the `Image` element to drop the `data-rk` attribute so
only `wrapperProps={{ hw: tokenLogoHw, "data-rk": "token-logo" }}` remains,
ensuring TypeScript strict props validation passes.

Comment on lines +107 to 109
<Box key={virtualItem.key} data-index={virtualItem.index}>
{itemContent(virtualItem.index, data[virtualItem.index])}
</Box>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does the @tanstack/react-virtualversion used by this repo still require wiringvirtualizer.measureElementthrough each row'sreftogether withdata-index for dynamically sized rows?

💡 Result:

Yes, the @tanstack/react-virtual library (latest version 3.13.24 as of April 2026) still requires wiring virtualizer.measureElement through each row's ref along with the data-index attribute for dynamically sized rows. This is the standard and recommended pattern for dynamic measurement in all official documentation, examples, and recent issues. Official TanStack Virtual docs explicitly state: "You are responsible for calling this [measureElement] in your virtualizer markup when the component is rendered (eg. using something like React's ref callback prop) also adding data-index" with example:

TanStack Table v8 virtualization examples (which use @tanstack/react-virtual) for dynamic row heights show identical pattern: <tr data-index={virtualRow.index} ref={(node) => rowVirtualizer.measureElement(node)} ... > Recent GitHub issues from 2025 (e.g., #1063, #1036 on v3.13.x) confirm this remains required, with bugs specifically referencing the need for data-index and measureElement. A 2024 PR (#737) added optional virtualItem.measureElement for special cases (e.g., Lit integration where node.isConnected is unreliable), but the primary API for React remains virtualizer.measureElement + data-index. No changelog entries or releases indicate this pattern has changed in v3.x up to 3.13.24. Blogs and tutorials from 2026 (e.g., viprasol.com) continue using this exact approach for dynamic rows.

Citations:


Restore row measurement for variable-height items.

Lines 107 and 232 remove the only call site that lets TanStack Virtual capture actual row heights. measureElement is the required API for the virtualizer to track actual row heights; without it, getTotalSize() and item offsets remain estimate-based. When row heights differ from estimateSize, this produces mispositioned items and unreliable end-of-list detection. The current version (v3.13.24) requires wiring this ref with data-index on each rendered row.

Suggested fix
-            <Box key={virtualItem.key} data-index={virtualItem.index}>
+            <Box
+              key={virtualItem.key}
+              data-index={virtualItem.index}
+              ref={rowVirtualizer.measureElement}
+            >
               {itemContent(virtualItem.index, data[virtualItem.index])}
             </Box>
-              <Box data-index={virtualItem.index} key={virtualItem.index}>
+              <Box
+                data-index={virtualItem.index}
+                key={virtualItem.index}
+                ref={rowVirtualizer.measureElement}
+              >
                 {type === "child"
                   ? itemContent(item.index, item.parentIndex)
                   : groupContent(item.index)}
               </Box>

Also applies to: 232-236

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

In `@packages/widget/src/components/atoms/virtual-list/index.tsx` around lines 107
- 109, The virtualizer lost its measureElement wiring so variable-height rows
are using estimates; restore the required ref on each rendered row so TanStack
Virtual can capture actual heights by passing the element and index to
measureElement. In the component that renders rows (where Box and virtualItem
are used and itemContent is called), add a ref that calls measureElement(el,
virtualItem.index) (or the equivalent measureElement API from your virtualizer
instance) on the Box rendering each virtualItem, keeping the existing data-index
attribute so the virtualizer can correlate elements to indexes.

Comment on lines +10 to +11
const getTokenGetTokensQueryKey = (params?: TokenGetTokensParams) =>
["/v1/tokens", ...(params ? [params] : [])] as const;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'getTokenGetTokensQueryKey|enabledYieldsOnly|queryKey:' packages/widget/src/hooks/api/use-default-tokens.ts -C2

Repository: stakekit/widget

Length of output: 1040


🏁 Script executed:

cat -n packages/widget/src/hooks/api/use-default-tokens.ts

Repository: stakekit/widget

Length of output: 2290


Include enabledYieldsOnly in the query key to avoid stale cache on setting changes.

The queryFn depends on enabledYieldsOnly (line 23), but the query key only varies by network. When toggling the tokensForEnabledYieldsOnly setting, the cache key remains identical, causing stale data to be returned. This affects both useDefaultTokens (line 18) and getDefaultTokens (line 35–37).

Proposed fix
-const getTokenGetTokensQueryKey = (params?: TokenGetTokensParams) =>
+const getTokenGetTokensQueryKey = (
+  params?: Pick<TokenGetTokensParams, "network" | "enabledYieldsOnly">
+) =>
   ["/v1/tokens", ...(params ? [params] : [])] as const;

   return useQuery({
-    queryKey: getTokenGetTokensQueryKey({ network: network ?? undefined }),
+    queryKey: getTokenGetTokensQueryKey({
+      network: network ?? undefined,
+      enabledYieldsOnly: !!tokensForEnabledYieldsOnly || undefined,
+    }),
     queryFn: async () =>
       (
         await queryFn({
           network: network ?? undefined,
           enabledYieldsOnly: !!tokensForEnabledYieldsOnly,
         })
       ).unsafeCoerce(),
@@
       queryKey: getTokenGetTokensQueryKey({
         network: params.network ?? undefined,
+        enabledYieldsOnly: params.enabledYieldsOnly || undefined,
       }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/hooks/api/use-default-tokens.ts` around lines 10 - 11,
The query key generator getTokenGetTokensQueryKey currently only includes
TokenGetTokensParams (network) so toggling tokensForEnabledYieldsOnly
(enabledYieldsOnly) doesn't change the key and returns stale cache; update
getTokenGetTokensQueryKey to include enabledYieldsOnly (or accept a params shape
that contains enabledYieldsOnly) so the key becomes ["/v1/tokens", params?,
enabledYieldsOnly] (or equivalent), and update both callers useDefaultTokens and
getDefaultTokens to pass the enabledYieldsOnly value into the key generation so
the query key varies when the setting changes.

}) => {
const { t } = useTranslation();

const nameOrAddress = providerDetails.name ?? providerDetails ?? "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: providerDetails is an object, not a string.

The fallback providerDetails.name ?? providerDetails will result in [object Object] being used as nameOrAddress when name is undefined, since providerDetails is the destructured props object.

This should likely be:

🐛 Proposed fix
-  const nameOrAddress = providerDetails.name ?? providerDetails ?? "";
+  const nameOrAddress = providerDetails.name ?? providerDetails.address ?? "";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nameOrAddress = providerDetails.name ?? providerDetails ?? "";
const nameOrAddress = providerDetails.name ?? providerDetails.address ?? "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/pages/position-details/components/provider-details.tsx`
at line 42, The current assignment to nameOrAddress uses providerDetails
directly (const nameOrAddress = providerDetails.name ?? providerDetails ?? "";),
but providerDetails is an object so that falls back to "[object Object]"; change
the fallback to a scalar property (e.g., use providerDetails.address or
providerDetails.id) instead of the whole object — update the expression to
something like providerDetails.name ?? providerDetails.address ?? "" so
nameOrAddress is always a string; locate the constant nameOrAddress in
provider-details.tsx and replace the fallback accordingly.

Comment on lines +36 to +45
const actionPreviewQuery = useQuery({
enabled: !!exitRequest,
queryKey: ["unstake-review-action-preview", exitRequest.requestDto],
retry: false,
queryFn: () =>
createExitAction({
fetchClient: yieldApiFetchClient,
requestDto: exitRequest.requestDto,
}),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle action-preview failures explicitly.

This new preview query drives both stakeExitTxGas and the gas warning state, but its error path is ignored. If createExitAction fails, stakeExitTxGas becomes empty, gasCheckLoading flips to false, and the review can proceed with no fee/warning data instead of surfacing the failure.

Also applies to: 47-59, 185-188

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

In `@packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts` around
lines 36 - 45, The action-preview query (actionPreviewQuery) that calls
createExitAction currently ignores errors, which leaves stakeExitTxGas empty and
gasCheckLoading false so the review can proceed without fee/warning data; update
the queryFn/error handling to explicitly handle failures by capturing the error
(query's onError or error state), set a distinct error flag/state (e.g.,
actionPreviewError) and ensure stakeExitTxGas and gasCheckLoading reflect the
failed state (prevent proceeding or show an explicit error UI/message), and
propagate that error into any dependent logic that reads stakeExitTxGas or
gasCheckLoading (also apply the same explicit error handling pattern to the
other similar queries/usages referenced by the comment).

Comment on lines +111 to +116
if (
recentConnectorId &&
recentConnectorId === solanaWallet.adapter.name
) {
await solanaWallet.adapter.autoConnect();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In @solana/wallet-adapter(the version used in this repo), canautoConnect()reject, and is catching errors recommended for non-throwing auth checks likeisAuthorized?

💡 Result:

Yes, autoConnect can reject with a Promise that may throw WalletError subclasses (e.g., WalletNotConnectedError, WalletNotSelectedError, WalletConnectionError) based on wallet state and availability. Catching errors is recommended even for non-throwing auth checks like wallet connection status, as useWallet provides connected and publicKey booleans/states that are synchronous and reliable for authorization without async calls; however, any manual connect or autoConnect calls (e.g., in custom logic) should be wrapped in try-catch to handle uncaught promise rejections, as seen in GitHub issues where errors like WalletNotSelectedError occur without handling.

Citations:


🏁 Script executed:

cat -n packages/widget/src/providers/misc/solana-connector.ts | head -150 | tail -60

Repository: stakekit/widget

Length of output: 2131


Guard autoConnect() errors in isAuthorized

autoConnect() can reject with WalletError subclasses. Unhandled rejection causes isAuthorized to throw instead of returning false, breaking the reconnect/auth probe flow. Wrap the call in try-catch and fail closed.

Suggested fix
       if (
         recentConnectorId &&
         recentConnectorId === solanaWallet.adapter.name
       ) {
-        await solanaWallet.adapter.autoConnect();
+        if (!solanaWallet.adapter.connected) {
+          try {
+            await solanaWallet.adapter.autoConnect();
+          } catch {
+            return false;
+          }
+        }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
recentConnectorId &&
recentConnectorId === solanaWallet.adapter.name
) {
await solanaWallet.adapter.autoConnect();
}
if (
recentConnectorId &&
recentConnectorId === solanaWallet.adapter.name
) {
if (!solanaWallet.adapter.connected) {
try {
await solanaWallet.adapter.autoConnect();
} catch {
return false;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/src/providers/misc/solana-connector.ts` around lines 111 -
116, In isAuthorized, guard the call to solanaWallet.adapter.autoConnect() so
any rejection (WalletError subclasses) is caught and handled: wrap the await
solanaWallet.adapter.autoConnect() invocation in a try/catch, return false (fail
closed) when an error is caught and optionally log the error; keep the existing
logic that only attempts autoConnect when recentConnectorId && recentConnectorId
=== solanaWallet.adapter.name.

Comment on lines +72 to +79
export const yieldApiYieldFixture = (
overrides?: Partial<YieldApiYieldDto>
): YieldApiYieldDto =>
({
...getYieldV2ControllerGetYieldByIdResponseMock(),
rewardRate: yieldRewardRateFixture(),
...overrides,
}) as YieldApiYieldDto;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

yieldApiYieldFixture is still returning a legacy payload shape.

This helper currently just rebrands getYieldV2ControllerGetYieldByIdResponseMock() with a cast, so fields like mechanics, inputTokens, outputToken, network, and providerId stay missing unless every caller remembers to patch them in. That makes the new fixture brittle and can hide schema drift in tests. Please build it from explicit Yield API defaults or route it through yieldApiYieldFixtureFromLegacy(...) instead of relying on the cast.

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

In `@packages/widget/tests/fixtures/index.ts` around lines 72 - 79,
yieldApiYieldFixture currently returns a legacy-shaped payload by simply casting
getYieldV2ControllerGetYieldByIdResponseMock(), leaving fields like mechanics,
inputTokens, outputToken, network, and providerId absent; replace the cast
approach by either (a) constructing the fixture from explicit Yield API defaults
(set mechanics, inputTokens, outputToken, network, providerId, rewardRate, etc.)
in the body of yieldApiYieldFixture, merging in overrides, or (b) delegate to
the existing helper yieldApiYieldFixtureFromLegacy(...) so the legacy mock is
normalized into the new schema before applying overrides; update
yieldApiYieldFixture to use one of these approaches and ensure all required
YieldApiYieldDto fields are present.

Comment on lines +345 to +355
const requestFn = vitest.fn(async ({ method }: { method: string }) => {
await waitForMs(100);

switch (method) {
case "eth_chainId":
return avalanche.id;
case "eth_requestAccounts":
return [account];
default:
throw new Error("unhandled method");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the file and lines mentioned
cd packages/widget/tests/use-cases/trust-incentive-apy
wc -l setup.ts

Repository: stakekit/widget

Length of output: 72


🏁 Script executed:

# Read the specific lines in question
sed -n '345,355p' packages/widget/tests/use-cases/trust-incentive-apy/setup.ts

Repository: stakekit/widget

Length of output: 363


🏁 Script executed:

# Let's also see broader context around this to understand the setup
sed -n '340,365p' packages/widget/tests/use-cases/trust-incentive-apy/setup.ts

Repository: stakekit/widget

Length of output: 591


🏁 Script executed:

# Find the avalanche object definition to check what type avalanche.id is
rg "avalanche\s*=" packages/widget/tests/use-cases/trust-incentive-apy/setup.ts -A 5

Repository: stakekit/widget

Length of output: 41


🏁 Script executed:

# Also search in related chain definition files
fd -i "chain\|avalanche" packages/widget/tests --type f | head -20

Repository: stakekit/widget

Length of output: 41


🏁 Script executed:

# Search for avalanche import or definition in the setup file
head -100 packages/widget/tests/use-cases/trust-incentive-apy/setup.ts | rg -i "avalanche|import"

Repository: stakekit/widget

Length of output: 457


🏁 Script executed:

# Search in the entire packages/widget/tests directory
rg "export.*avalanche|const avalanche" packages/widget/tests --type ts -B 2 -A 2

Repository: stakekit/widget

Length of output: 3131


🏁 Script executed:

# Check for common chain definitions, including avalanche
rg "avalanche|chainId" packages/widget/tests/use-cases/trust-incentive-apy/setup.ts | head -20

Repository: stakekit/widget

Length of output: 329


🌐 Web query:

JSON-RPC eth_chainId response format EIP-1193 specification

💡 Result:

The JSON-RPC eth_chainId method returns the current chain ID as a hexadecimal string (e.g., "0x1" for Ethereum mainnet). This is defined in EIP-695 and referenced in EIP-1193 for Ethereum Provider JavaScript APIs. Example JSON-RPC response: { "jsonrpc": "2.0", "id": 1, "result": "0x1" } In EIP-1193 providers (e.g., window.ethereum), calling provider.request({ method: 'eth_chainId' }) returns the same hexadecimal string. The chainId property and chainChanged event also use this hex string format per eth_chainId.

Citations:


Return a hex chain id from the wallet mock.

eth_chainId responses are hex strings per the JSON-RPC specification (EIP-695, EIP-1193), not numbers. The mock currently returns avalanche.id as a numeric value, which differs from production wallets and can hide parsing bugs in the connector flow.

Suggested fix
     switch (method) {
       case "eth_chainId":
-        return avalanche.id;
+        return `0x${avalanche.id.toString(16)}`;
       case "eth_requestAccounts":
         return [account];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const requestFn = vitest.fn(async ({ method }: { method: string }) => {
await waitForMs(100);
switch (method) {
case "eth_chainId":
return avalanche.id;
case "eth_requestAccounts":
return [account];
default:
throw new Error("unhandled method");
}
const requestFn = vitest.fn(async ({ method }: { method: string }) => {
await waitForMs(100);
switch (method) {
case "eth_chainId":
return `0x${avalanche.id.toString(16)}`;
case "eth_requestAccounts":
return [account];
default:
throw new Error("unhandled method");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/widget/tests/use-cases/trust-incentive-apy/setup.ts` around lines
345 - 355, The wallet mock `requestFn` currently returns `avalanche.id` (a
number) for the "eth_chainId" case; update the "eth_chainId" branch in the
`requestFn` (the vitest.fn mock) to return a hex string per JSON-RPC (e.g.
convert `avalanche.id` to a "0x..." hex string) instead of a numeric value so
the connector receives the same format as real wallets.

@petar-omni petar-omni force-pushed the feat/yield-balances-migration branch from dfbc459 to bf2f956 Compare April 28, 2026 17:09
Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
mise.toml (1)

2-3: Add engines.node to package.json to enforce Node version outside mise

Node 24 is pinned in mise.toml and CI, but contributors who bypass mise (or use different package managers) won't have this constraint enforced. Add engines.node to package.json for consistency across all tooling paths.

Proposed change
 {
   "packageManager": "pnpm@10.33.2",
+  "engines": {
+    "node": "24.x"
+  }
 }

After applying this change, run npm run lint (or pnpm lint) locally to confirm no regressions.

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

In `@mise.toml` around lines 2 - 3, The repo pins Node 24 in mise.toml but lacks a
corresponding engines.node entry in package.json; add an "engines": { "node":
">=24 <25" } (or the exact range you want) to package.json to enforce Node
version for npm/pnpm users, commit that change, and run the linter (npm run lint
or pnpm lint) to ensure no lint/regression issues; locate package.json and
update its top-level "engines" field (or add it if missing).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@mise.toml`:
- Around line 2-3: The repo pins Node 24 in mise.toml but lacks a corresponding
engines.node entry in package.json; add an "engines": { "node": ">=24 <25" } (or
the exact range you want) to package.json to enforce Node version for npm/pnpm
users, commit that change, and run the linter (npm run lint or pnpm lint) to
ensure no lint/regression issues; locate package.json and update its top-level
"engines" field (or add it if missing).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 67a6cca9-3034-4aed-b846-34d0bde63fb8

📥 Commits

Reviewing files that changed from the base of the PR and between dfbc459 and 2511e53.

📒 Files selected for processing (2)
  • mise.toml
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • package.json

@aws-amplify-eu-central-1
Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-516.d2ribjy8evqo6h.amplifyapp.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant