Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lemon-drinks-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stakekit/widget": patch
---

feat: migrate to yield api balances
46 changes: 46 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# StakeKit Widget — Agent Guide

## Project Overview
- Monorepo managed with `pnpm` workspaces + Turborepo.
- Main package is `@stakekit/widget` in `packages/widget` (React + TypeScript + Vite).
- Widget supports two entry modes:
- React component export (`src/index.package.ts`)
- Fully bundled renderer (`src/index.bundle.ts`)
- Runtime branches between classic widget and dashboard variant in `src/App.tsx`.

## Repo Layout (important paths)
- `packages/widget/src/App.tsx` — root app, router setup, bundle renderer.
- `packages/widget/src/Widget.tsx` — non-dashboard route flow (earn/review/steps/details).
- `packages/widget/src/Dashboard.tsx` + `pages-dashboard/*` — dashboard variant UI.
- `packages/widget/src/providers/*` — global provider composition (API, query, wallet, tracking, theme, stores).
- `packages/widget/src/hooks/*` — feature and API hooks.
- `packages/widget/src/domain/*` — shared domain types/helpers.
- `packages/widget/src/translation/*` — i18n resources (`English`, `French`).
- `packages/widget/tests/*` — Vitest browser tests (MSW-backed).
- `packages/examples/*` — integration examples (`with-vite`, `with-vite-bundled`, `with-nextjs`, `with-cdn-script`).

## Commands Agents Should Use

### From repo root (all workspaces via Turbo)
- `pnpm build` — build all packages.
- `pnpm lint` — lint/type-check all packages.
- `pnpm test` — run all workspace tests.
- `pnpm format` — run formatting checks/tasks.

### Focused widget commands (recommended for most tasks)
- `pnpm --filter @stakekit/widget {command}`

## Agent Working Guidelines (short)
- Keep public API compatibility in `src/index.package.ts` and `src/index.bundle.ts`.
- When changing user-facing copy, update both:
- `packages/widget/src/translation/English/translations.json`
- `packages/widget/src/translation/French/translations.json`
- After changes, confirm nothing is broken with lint command which checks lint/type errors

## Useful Context for Debugging
- API client is configured in `packages/widget/src/providers/api/api-client-provider.tsx`.
- React Query defaults are in `packages/widget/src/providers/query-client/index.tsx`.
- App-level config/env mapping is in `packages/widget/src/config/index.ts`.
- Test bootstrapping + MSW worker setup:
- `packages/widget/tests/utils/setup.ts`
- `packages/widget/tests/mocks/worker.ts`
4 changes: 2 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[tools]
node = "22"
pnpm = "10"
node = "24"
pnpm = "10"
6 changes: 5 additions & 1 deletion packages/widget/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"clean": "rm -rf dist",
"preview": "vite -c vite/vite.config.dev.ts preview --outDir dist/website",
"check-unused": "npx knip",
"check-circular-deps": "skott ./src/index.package.ts -m 'raw' && pnpm lint"
"check-circular-deps": "skott ./src/index.package.ts -m 'raw' && pnpm lint",
"gen:yield-api": "openapi-typescript https://docs.yield.xyz/openapi/appsyield-apiswagger-docsopenapi.yaml -o src/types/yield-api-schema.d.ts"
},
"peerDependencies": {
"react": ">=18",
Expand Down Expand Up @@ -130,6 +131,9 @@
"mixpanel-browser": "^2.72.0",
"motion": "12.23.26",
"msw": "^2.12.4",
"openapi-fetch": "^0.17.0",
"openapi-react-query": "^0.5.4",
"openapi-typescript": "^7.13.0",
"playwright": "^1.57.0",
"postcss": "^8.5.6",
"purify-ts": "2.1.0",
Expand Down
19 changes: 0 additions & 19 deletions packages/widget/src/common/private-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import {
type TokenBalanceScanDto,
type TokenBalanceScanResponseDto,
type ValidatorSearchResultDto,
type YieldBalanceScanRequestDto,
type YieldBalancesWithIntegrationIdDto,
type YieldDto,
} from "@stakekit/api-hooks";

Expand All @@ -25,23 +23,6 @@ export const tokenTokenBalancesScan = (
});
};

/**
* Scans for yield balances among enabled yields.
* @summary Scan for yield balances
*/
export const yieldYieldBalancesScan = (
yieldBalanceScanRequestDto: YieldBalanceScanRequestDto,
signal?: AbortSignal
) => {
return customFetch<YieldBalancesWithIntegrationIdDto[]>({
url: "/v1/yields/balances/scan",
method: "POST",
headers: { "Content-Type": "application/json" },
data: yieldBalanceScanRequestDto,
signal,
});
};

/**
* Returns a yield that is associated with given integration ID
* @summary Get a yield given an integration ID
Expand Down
3 changes: 2 additions & 1 deletion packages/widget/src/components/atoms/token-icon/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TokenDto, YieldMetadataDto } from "@stakekit/api-hooks";
import { useSettings } from "../../../providers/settings";
import type { YieldTokenDto } from "../../../providers/yield-api-client-provider/types";
import type { Atoms } from "../../../styles/theme/atoms.css";
import { NetworkLogoImage } from "./network-icon-image";
import { TokenIconContainer } from "./token-icon-container";
Expand All @@ -12,7 +13,7 @@ export const TokenIcon = ({
tokenNetworkLogoHw,
hideNetwork,
}: {
token: TokenDto;
token: TokenDto | YieldTokenDto;
metadata?: YieldMetadataDto;
tokenLogoHw?: Atoms["hw"];
tokenNetworkLogoHw?: Atoms["hw"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { Maybe } from "purify-ts";
import { useMemo } from "react";
import { config } from "../../../../../config";
import { useSettings } from "../../../../../providers/settings";
import type { YieldTokenDto } from "../../../../../providers/yield-api-client-provider/types";

export const useVariantTokenUrls = (
token: TokenDto,
token: TokenDto | YieldTokenDto,
metadata?: YieldMetadataDto
): {
mainUrl: string | undefined;
Expand Down Expand Up @@ -35,7 +36,7 @@ export const useVariantTokenUrls = (
const tokenMappingResult = Maybe.fromNullable(tokenIconMapping)
.chainNullable((mapping) => {
if (typeof mapping === "function") {
return mapping(token);
return mapping(token as TokenDto);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's read the target file to understand the context around line 39
cat -n packages/widget/src/components/atoms/token-icon/token-icon-container/hooks/use-variant-token-urls.ts | head -60

Repository: stakekit/widget

Length of output: 2205


🏁 Script executed:

# Search for tokenIconMapping to understand its type signature
rg -n 'tokenIconMapping' packages/widget/src --type ts --type tsx -B 2 -A 2

Repository: stakekit/widget

Length of output: 86


🏁 Script executed:

# Look for TokenDto and YieldTokenDto type definitions
rg -n 'type TokenDto|interface TokenDto|type YieldTokenDto|interface YieldTokenDto' packages/widget/src --type ts --type tsx

Repository: stakekit/widget

Length of output: 86


🏁 Script executed:

# Check the settings provider or context that defines the callback
fd 'settings' packages/widget/src/providers --type ts --type tsx

Repository: stakekit/widget

Length of output: 228


🏁 Script executed:

# Search for tokenIconMapping definition and type
rg -n 'tokenIconMapping' packages/widget/src -B 2 -A 5

Repository: stakekit/widget

Length of output: 3888


🏁 Script executed:

# Look for TokenDto type definition
rg -n 'type TokenDto|interface TokenDto' packages/widget/src

Repository: stakekit/widget

Length of output: 319


🏁 Script executed:

# Look for YieldTokenDto type definition
rg -n 'type YieldTokenDto|interface YieldTokenDto' packages/widget/src

Repository: stakekit/widget

Length of output: 189


🏁 Script executed:

# Find settings provider or useSettings hook
fd 'settings' packages/widget/src/providers

Repository: stakekit/widget

Length of output: 98


🏁 Script executed:

# Check if TokenDto and YieldTokenDto are compatible by viewing their definitions
rg -n 'export type YieldTokenDto' packages/widget/src -A 3

Repository: stakekit/widget

Length of output: 466


🏁 Script executed:

# Check the import source of TokenDto to understand what it is
cat -n packages/widget/src/providers/settings/types.ts | head -20

Repository: stakekit/widget

Length of output: 1014


🏁 Script executed:

# Look at the complete settings types file to see the context
cat -n packages/widget/src/providers/settings/types.ts

Repository: stakekit/widget

Length of output: 3504


Update tokenIconMapping callback type to accept the union instead of casting.

At Line 39, the cast suppresses type safety when the function parameter accepts TokenDto | YieldTokenDto. Since tokenIconMapping is defined to only accept TokenDto (in packages/widget/src/providers/settings/types.ts:68), update its callback signature to ((token: TokenDto | YieldTokenDto) => string) to properly reflect what the function can receive and eliminate the cast.

🤖 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`
at line 39, The callback type for tokenIconMapping must accept the union type
instead of relying on a cast: update the tokenIconMapping callback signature
(where it's declared in the settings types) to (token: TokenDto | YieldTokenDto)
=> string so mapping(token) no longer needs to cast; adjust the type declaration
in packages/widget/src/providers/settings/types.ts (the tokenIconMapping
definition) and any affected references (e.g., use-variant-token-urls.ts where
mapping is invoked) so the mapping function accepts the union directly.

}

return mapping[token.symbol];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { TokenDto, YieldMetadataDto } from "@stakekit/api-hooks";
import type { Networks } from "@stakekit/common";
import type { ReactElement } from "react";
import type { YieldTokenDto } from "../../../../providers/yield-api-client-provider/types";
import { Box } from "../../box";
import { useVariantNetworkUrls } from "./hooks/use-variant-network-urls";
import { useVariantTokenUrls } from "./hooks/use-variant-token-urls";

type TokenIconContainerProps = {
token: TokenDto;
token: TokenDto | YieldTokenDto;
metadata?: YieldMetadataDto;
hideNetwork?: boolean;
children: (props: TokenIconContainerReturnType) => ReactElement;
Expand Down
2 changes: 2 additions & 0 deletions packages/widget/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const config = {
appPrefix: "sk-widget",
env: {
apiUrl: import.meta.env.VITE_API_URL ?? "https://api.stakek.it/",
yieldsApiUrl:
import.meta.env.VITE_YIELDS_API_URL ?? "https://api.yield.xyz",
isTestMode: import.meta.env.MODE === "test",
isDevMode: import.meta.env.MODE === "development",
forceAddress: import.meta.env.VITE_FORCE_ADDRESS,
Expand Down
25 changes: 17 additions & 8 deletions packages/widget/src/domain/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {
ActionDto,
PendingActionDto,
TokenDto,
TransactionDto,
TransactionStatus,
Expand All @@ -9,15 +8,25 @@ import type {
import BigNumber from "bignumber.js";
import { Left, type Maybe, Right } from "purify-ts";
import type { Override } from "../types/utils";
import type { AnyPendingActionDto } from "./types/pending-action";
import {
isPendingActionValidatorAddressesRequired,
isPendingActionValidatorAddressRequired,
} from "./types/pending-action";
import type { TokenString } from "./types/tokens";

export { getTokenPriceInUSD } from "./types/price";

export const tokenString = (token: TokenDto): TokenString => {
return `${token.network}-${token.address?.toLowerCase()}`;
type TokenLike = Pick<TokenDto, "symbol"> & {
network: string;
address?: string;
};

export const equalTokens = (a: TokenDto, b: TokenDto) =>
export const tokenString = (token: TokenLike): TokenString => {
return `${token.network}-${token.address?.toLowerCase() ?? ""}` as TokenString;
};

export const equalTokens = (a: TokenLike, b: TokenLike) =>
tokenString(a) === tokenString(b) && a.symbol === b.symbol;

export const stakeTokenSameAsGasToken = ({
Expand Down Expand Up @@ -81,11 +90,11 @@ export const getValidStakeSessionTx = (stakeDto: ActionDto) => {
export const isTxError = (txStatus: TransactionStatus) =>
txStatus === "FAILED" || txStatus === "BLOCKED";

export const PAMultiValidatorsRequired = (pa: PendingActionDto) =>
!!pa.args?.args?.validatorAddresses?.required;
export const PAMultiValidatorsRequired = (pa: AnyPendingActionDto) =>
isPendingActionValidatorAddressesRequired(pa);

export const PASingleValidatorRequired = (pa: PendingActionDto) =>
!!pa.args?.args?.validatorAddress?.required;
export const PASingleValidatorRequired = (pa: AnyPendingActionDto) =>
isPendingActionValidatorAddressRequired(pa);

export const skNormalizeChainId = (chainId: string) => {
const cId = Number(chainId);
Expand Down
104 changes: 104 additions & 0 deletions packages/widget/src/domain/types/pending-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { PendingActionDto as LegacyPendingActionDto } from "@stakekit/api-hooks";
import type { YieldPendingActionDto } from "../../providers/yield-api-client-provider/types";

type PendingActionArgName =
| "amount"
| "validatorAddress"
| "validatorAddresses";

export type AnyPendingActionDto =
| LegacyPendingActionDto
| YieldPendingActionDto;

export type PendingActionAmountConfig = {
required: boolean;
minimum: number | null;
maximum: number | null;
forceMax: boolean;
};

export const isPendingActionAmountRequired = (
pendingAction: AnyPendingActionDto
) => !!getPendingActionAmountConfig(pendingAction)?.required;

export const isPendingActionValidatorAddressRequired = (
pendingAction: AnyPendingActionDto
) => !!getPendingActionArgument(pendingAction, "validatorAddress")?.required;

export const isPendingActionValidatorAddressesRequired = (
pendingAction: AnyPendingActionDto
) => !!getPendingActionArgument(pendingAction, "validatorAddresses")?.required;

export const getPendingActionAmountConfig = (
pendingAction: AnyPendingActionDto
): PendingActionAmountConfig | null => {
const amountArg = getPendingActionArgument(pendingAction, "amount");

if (!amountArg) {
return null;
}

const minimum = toNumberOrNull(amountArg.minimum);
const maximum = toNumberOrNull(amountArg.maximum);

return {
required: !!amountArg.required,
minimum,
maximum,
forceMax: minimum === -1 && maximum === -1,
};
Comment on lines +41 to +49
Copy link

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
# First, find the file and verify its structure
echo "=== Checking pending-action.ts structure ==="
head -n 60 packages/widget/src/domain/types/pending-action.ts | cat -n

echo -e "\n=== Finding toNumberOrNull definition ==="
rg -n "toNumberOrNull" packages/widget/src/domain/types/pending-action.ts

echo -e "\n=== Finding toNumberOrNull function ==="
rg -B5 -A10 "export.*function toNumberOrNull|const toNumberOrNull.*=" packages/widget/src/

Repository: stakekit/widget

Length of output: 3776


🏁 Script executed:

#!/bin/bash
# Check downstream usage of getPendingActionAmountConfig and amountConfig
echo "=== Finding getPendingActionAmountConfig usages ==="
rg -n "getPendingActionAmountConfig" packages/widget/src/

echo -e "\n=== Finding where amountConfig.minimum and amountConfig.maximum are used ==="
rg -B3 -A3 "amountConfig\.(minimum|maximum)" packages/widget/src/

Repository: stakekit/widget

Length of output: 690


🏁 Script executed:

#!/bin/bash
# Check the complete function at lines 41-49 and 93-104
echo "=== Full context around lines 41-49 ==="
sed -n '30,55p' packages/widget/src/domain/types/pending-action.ts | cat -n

echo -e "\n=== Full context around lines 93-104 ==="
sed -n '85,115p' packages/widget/src/domain/types/pending-action.ts | cat -n

Repository: stakekit/widget

Length of output: 1586


🏁 Script executed:

#!/bin/bash
# Check the full context of amountConfig usage in position-details/state/index.tsx
echo "=== Context around line 212 in position-details/state/index.tsx ==="
sed -n '200,250p' packages/widget/src/pages/position-details/state/index.tsx | cat -n

echo -e "\n=== Search for BigNumber usage with minimum/maximum ==="
rg -B5 -A5 "new BigNumber.*minimum|new BigNumber.*maximum|BigNumber.*amountConfig" packages/widget/src/pages/position-details/

Repository: stakekit/widget

Length of output: 3022


🏁 Script executed:

#!/bin/bash
# Check what happens after amountConfig is retrieved
echo "=== Full usage pattern of amountConfig ==="
rg -B3 -A10 "const amountConfig = getPendingActionAmountConfig" packages/widget/src/pages/position-details/state/index.tsx

Repository: stakekit/widget

Length of output: 623


Preserve amount bounds as strings to avoid Number precision loss.

Lines 41–42 coerce minimum/maximum through toNumberOrNull, which calls Number(value). Large numeric strings (common in token amounts) lose precision in JavaScript's 53-bit integer limit before reaching BigNumber at line 214–217 of position-details/state/index.tsx. This produces incorrect min/max clamping.

Recommended fix: Keep values as number | string | null in PendingActionAmountConfig, pass strings directly to BigNumber (which handles them precisely), and only coerce to number for the forceMax check that requires -1 comparison.

♻️ Proposed fix
 export type PendingActionAmountConfig = {
   required: boolean;
-  minimum: number | null;
-  maximum: number | null;
+  minimum: number | string | null;
+  maximum: number | string | null;
   forceMax: boolean;
 };
@@
-  const minimum = toNumberOrNull(amountArg.minimum);
-  const maximum = toNumberOrNull(amountArg.maximum);
+  const minimum = amountArg.minimum ?? null;
+  const maximum = amountArg.maximum ?? null;
@@
   return {
     required: !!amountArg.required,
     minimum,
     maximum,
-    forceMax: minimum === -1 && maximum === -1,
+    forceMax: toNumberOrNull(minimum) === -1 && toNumberOrNull(maximum) === -1,
   };
 };

Also applies to: 93–104

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

In `@packages/widget/src/domain/types/pending-action.ts` around lines 41 - 49, The
amount bounds are being coerced to Number via toNumberOrNull which causes
precision loss for large token strings; change the handling so minimum and
maximum are preserved as number | string | null in PendingActionAmountConfig
(remove toNumberOrNull when reading amountArg.minimum/maximum), pass those
string values directly into BigNumber where needed, and only perform a numerical
-1 check for forceMax (e.g., coerce to number or compare to the string "-1")
when computing forceMax in the function that builds the config (refer to
minimum, maximum, forceMax and PendingActionAmountConfig); apply the same change
to the other occurrence around lines 93–104 so both places preserve string
bounds and only coerce for the -1 comparison.

};

const getPendingActionArgument = (
pendingAction: AnyPendingActionDto,
name: PendingActionArgName
) => {
const v2Field = (
pendingAction as YieldPendingActionDto
).arguments?.fields?.find(
(
field: NonNullable<YieldPendingActionDto["arguments"]>["fields"][number]
) => field.name === name
);

if (v2Field) {
return {
required: !!v2Field.required,
minimum: v2Field.minimum ?? null,
maximum: v2Field.maximum ?? null,
};
}

const legacyField = (pendingAction as LegacyPendingActionDto).args?.args?.[
name
] as
| {
required?: boolean;
minimum?: number | string | null;
maximum?: number | string | null;
}
| undefined;

if (!legacyField) {
return null;
}

return {
required: !!legacyField.required,
minimum: legacyField.minimum ?? null,
maximum: legacyField.maximum ?? null,
};
};

const toNumberOrNull = (value: number | string | null | undefined) => {
if (value === null || value === undefined) {
return null;
}

if (typeof value === "number") {
return Number.isFinite(value) ? value : null;
}

const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
};
Loading