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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,7 @@ next-env.d.ts
*.p12
*.pfx
*.crt
*.cer
*.cer

# opensrc - source code for packages
opensrc/
69 changes: 69 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 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`

<!-- opensrc:start -->

## Source Code Reference

Source code for dependencies is available in `opensrc/` for deeper understanding of implementation details.

See `opensrc/sources.json` for the list of available packages and their versions.

Use this source code when you need to understand how a package works internally, not just its types/interface.

### Fetching Additional Source Code

To fetch source code for a package or repository you need to understand, run:

```bash
npx opensrc <package> # npm package (e.g., npx opensrc zod)
npx opensrc pypi:<package> # Python package (e.g., npx opensrc pypi:requests)
npx opensrc crates:<package> # Rust crate (e.g., npx opensrc crates:serde)
npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)
```

<!-- opensrc:end -->
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"
8 changes: 6 additions & 2 deletions 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://api.stg.yield.xyz/docs.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 Expand Up @@ -160,4 +164,4 @@
"public"
]
}
}
}
4 changes: 3 additions & 1 deletion packages/widget/src/common/delay-api-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ const checkDelay = () => {
}).then(() => unsub());
};

export const waitForDelayedApiRequests = () => checkDelay();

export const attachDelayInterceptor = (apiClient: AxiosInstance) =>
apiClient.interceptors.response.use(async (response) => {
await checkDelay();
await waitForDelayedApiRequests();

return response;
});
Expand Down
42 changes: 0 additions & 42 deletions packages/widget/src/common/private-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ import {
customFetch,
type TokenBalanceScanDto,
type TokenBalanceScanResponseDto,
type ValidatorSearchResultDto,
type YieldBalanceScanRequestDto,
type YieldBalancesWithIntegrationIdDto,
type YieldDto,
} from "@stakekit/api-hooks";

Expand All @@ -25,23 +22,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 All @@ -59,25 +39,3 @@ export const yieldYieldOpportunity = (
signal,
});
};

export type YieldFindValidatorsParams = {
ledgerWalletAPICompatible?: boolean;
network?: string;
query?: string;
};

/**
* Returns a list of available validators to specify when providing a `validatorAddress` property.
* @summary Get validators
*/
export const yieldFindValidators = (
params?: YieldFindValidatorsParams,
signal?: AbortSignal
) => {
return customFetch<ValidatorSearchResultDto[]>({
url: "/v1/yields/validators",
method: "GET",
params,
signal,
});
};
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useTranslation } from "react-i18next";
import {
getRewardRateBreakdown,
type RewardRateBreakdownItem,
} from "../../../domain/types/reward-rate";
import type { YieldRewardRateDto } from "../../../providers/yield-api-client-provider/types";
import { getRewardRateFormatted } from "../../../utils/formatters";
import { Box } from "../../atoms/box";
import { Text } from "../../atoms/typography/text";

const getLabelKey = (key: RewardRateBreakdownItem["key"]) => {
switch (key) {
case "native":
return "details.apy_composition.native";
case "protocol_incentive":
return "details.apy_composition.protocol_incentive";
case "campaign":
return "details.apy_composition.campaign";
}
};

export const RewardRateBreakdown = ({
rewardRate,
showUpToCampaign = false,
title,
testId,
}: {
rewardRate: YieldRewardRateDto | null | undefined;
showUpToCampaign?: boolean;
title?: string;
testId?: string;
}) => {
const { t } = useTranslation();

const items = getRewardRateBreakdown(rewardRate, {
showUpToCampaign,
});

if (!items.length) {
return null;
}

return (
<Box
display="flex"
flexDirection="column"
gap="2"
marginTop="3"
data-testid={testId}
>
{title ? (
<Text variant={{ type: "muted", weight: "normal" }}>{title}</Text>
) : null}

{items.map((item) => {
const value = getRewardRateFormatted({
rewardRate: item.rate,
rewardType: item.rewardType,
});

return (
<Box
key={item.key}
display="flex"
justifyContent="space-between"
alignItems="center"
gap="3"
data-testid={
testId ? `${testId}__${item.key.replaceAll("_", "-")}` : undefined
}
>
<Text variant={{ type: "muted", weight: "normal" }}>
{t(getLabelKey(item.key))}
</Text>

<Text variant={{ type: "muted", weight: "normal" }}>
{item.isUpTo
? t("details.apy_composition.up_to", { value })
: value}
</Text>
</Box>
);
})}
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ActionTypes } from "@stakekit/api-hooks";
import { Maybe } from "purify-ts";
import type { ComponentProps } from "react";
import { Trans } from "react-i18next";
import type { useRewardTokenDetails } from "../../../hooks/use-reward-token-details";
import type { YieldPendingActionType } from "../../../providers/yield-api-client-provider/types";
import { Box } from "../../atoms/box";
import { MorphoStarsIcon } from "../../atoms/icons/morpho-stars";
import { Image } from "../../atoms/image";
Expand All @@ -19,7 +19,7 @@ export const RewardTokenDetails = ({
| { type: "stake" | "unstake"; pendingAction?: never }
| {
type: "pendingAction";
pendingAction: ActionTypes;
pendingAction: YieldPendingActionType;
}
)) => {
const i18nKey: ComponentProps<typeof Trans>["i18nKey"] = (() => {
Expand All @@ -29,7 +29,7 @@ export const RewardTokenDetails = ({

if (rest.type === "pendingAction") {
return `pending_action_review.pending_action_type.${
rest.pendingAction.toLowerCase() as Lowercase<ActionTypes>
rest.pendingAction.toLowerCase() as Lowercase<YieldPendingActionType>
}` as const;
}

Expand Down
Loading
Loading