diff --git a/.changeset/lemon-drinks-prove.md b/.changeset/lemon-drinks-prove.md new file mode 100644 index 00000000..ae8fa51c --- /dev/null +++ b/.changeset/lemon-drinks-prove.md @@ -0,0 +1,5 @@ +--- +"@stakekit/widget": patch +--- + +feat: migrate to yield api balances diff --git a/.gitignore b/.gitignore index 69017cc3..70f97094 100644 --- a/.gitignore +++ b/.gitignore @@ -266,4 +266,7 @@ next-env.d.ts *.p12 *.pfx *.crt -*.cer \ No newline at end of file +*.cer + +# opensrc - source code for packages +opensrc/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..bcdce047 --- /dev/null +++ b/AGENTS.md @@ -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` + + + +## 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 # npm package (e.g., npx opensrc zod) +npx opensrc pypi: # Python package (e.g., npx opensrc pypi:requests) +npx opensrc crates: # Rust crate (e.g., npx opensrc crates:serde) +npx opensrc / # GitHub repo (e.g., npx opensrc vercel/ai) +``` + + \ No newline at end of file diff --git a/mise.toml b/mise.toml index e264c145..984ab6e7 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,3 @@ [tools] -node = "22" -pnpm = "10" \ No newline at end of file +node = "24" +pnpm = "10" diff --git a/packages/widget/package.json b/packages/widget/package.json index 8d8f9017..3c085ea5 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -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", @@ -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", @@ -160,4 +164,4 @@ "public" ] } -} \ No newline at end of file +} diff --git a/packages/widget/src/common/delay-api-requests.ts b/packages/widget/src/common/delay-api-requests.ts index df2c5ece..9e543ada 100644 --- a/packages/widget/src/common/delay-api-requests.ts +++ b/packages/widget/src/common/delay-api-requests.ts @@ -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; }); diff --git a/packages/widget/src/common/private-api.ts b/packages/widget/src/common/private-api.ts index 23df6ebb..7eea9757 100644 --- a/packages/widget/src/common/private-api.ts +++ b/packages/widget/src/common/private-api.ts @@ -2,9 +2,6 @@ import { customFetch, type TokenBalanceScanDto, type TokenBalanceScanResponseDto, - type ValidatorSearchResultDto, - type YieldBalanceScanRequestDto, - type YieldBalancesWithIntegrationIdDto, type YieldDto, } from "@stakekit/api-hooks"; @@ -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({ - 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 @@ -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({ - url: "/v1/yields/validators", - method: "GET", - params, - signal, - }); -}; diff --git a/packages/widget/src/components/atoms/token-icon/index.tsx b/packages/widget/src/components/atoms/token-icon/index.tsx index a237dbe7..e6a90eeb 100644 --- a/packages/widget/src/components/atoms/token-icon/index.tsx +++ b/packages/widget/src/components/atoms/token-icon/index.tsx @@ -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"; @@ -12,7 +13,7 @@ export const TokenIcon = ({ tokenNetworkLogoHw, hideNetwork, }: { - token: TokenDto; + token: TokenDto | YieldTokenDto; metadata?: YieldMetadataDto; tokenLogoHw?: Atoms["hw"]; tokenNetworkLogoHw?: Atoms["hw"]; diff --git a/packages/widget/src/components/atoms/token-icon/token-icon-container/hooks/use-variant-token-urls.ts b/packages/widget/src/components/atoms/token-icon/token-icon-container/hooks/use-variant-token-urls.ts index 94930944..cdfa3c0c 100644 --- a/packages/widget/src/components/atoms/token-icon/token-icon-container/hooks/use-variant-token-urls.ts +++ b/packages/widget/src/components/atoms/token-icon/token-icon-container/hooks/use-variant-token-urls.ts @@ -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; @@ -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); } return mapping[token.symbol]; diff --git a/packages/widget/src/components/atoms/token-icon/token-icon-container/index.tsx b/packages/widget/src/components/atoms/token-icon/token-icon-container/index.tsx index a17d64f8..9b587b46 100644 --- a/packages/widget/src/components/atoms/token-icon/token-icon-container/index.tsx +++ b/packages/widget/src/components/atoms/token-icon/token-icon-container/index.tsx @@ -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; diff --git a/packages/widget/src/components/molecules/reward-rate-breakdown/index.tsx b/packages/widget/src/components/molecules/reward-rate-breakdown/index.tsx new file mode 100644 index 00000000..f629003b --- /dev/null +++ b/packages/widget/src/components/molecules/reward-rate-breakdown/index.tsx @@ -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 ( + + {title ? ( + {title} + ) : null} + + {items.map((item) => { + const value = getRewardRateFormatted({ + rewardRate: item.rate, + rewardType: item.rewardType, + }); + + return ( + + + {t(getLabelKey(item.key))} + + + + {item.isUpTo + ? t("details.apy_composition.up_to", { value }) + : value} + + + ); + })} + + ); +}; diff --git a/packages/widget/src/components/molecules/reward-token-details/index.tsx b/packages/widget/src/components/molecules/reward-token-details/index.tsx index ff41a1ae..d982c275 100644 --- a/packages/widget/src/components/molecules/reward-token-details/index.tsx +++ b/packages/widget/src/components/molecules/reward-token-details/index.tsx @@ -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"; @@ -19,7 +19,7 @@ export const RewardTokenDetails = ({ | { type: "stake" | "unstake"; pendingAction?: never } | { type: "pendingAction"; - pendingAction: ActionTypes; + pendingAction: YieldPendingActionType; } )) => { const i18nKey: ComponentProps["i18nKey"] = (() => { @@ -29,7 +29,7 @@ export const RewardTokenDetails = ({ if (rest.type === "pendingAction") { return `pending_action_review.pending_action_type.${ - rest.pendingAction.toLowerCase() as Lowercase + rest.pendingAction.toLowerCase() as Lowercase }` as const; } diff --git a/packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx b/packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx index 7d83af4c..ecde1dee 100644 --- a/packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx +++ b/packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx @@ -3,6 +3,10 @@ import BigNumber from "bignumber.js"; import { Maybe } from "purify-ts"; import type { ComponentProps, ReactNode } from "react"; import { useTranslation } from "react-i18next"; +import { + getRewardRateBreakdown, + getYieldRewardRateDetails, +} from "../../../domain/types/reward-rate"; import { APToPercentage, formatNumber, fromWei } from "../../../utils"; import { getRewardRateFormatted } from "../../../utils/formatters"; import { Box } from "../../atoms/box"; @@ -29,6 +33,22 @@ export const SelectOpportunityListItem = ({ const { t } = useTranslation(); + const campaignRate = getRewardRateBreakdown( + getYieldRewardRateDetails(item) + ).find((rewardRate) => rewardRate.key === "campaign"); + + const totalRateFormatted = getRewardRateFormatted({ + rewardRate: item.rewardRate, + rewardType: item.rewardType, + }); + + const primaryRateFormatted = getRewardRateFormatted({ + rewardRate: campaignRate + ? item.rewardRate - campaignRate.rate + : item.rewardRate, + rewardType: item.rewardType, + }); + return ( @@ -52,13 +72,16 @@ export const SelectOpportunityListItem = ({ - - - {getRewardRateFormatted({ - rewardRate: item.rewardRate, - rewardType: item.rewardType, - })} - + + {primaryRateFormatted} + + {campaignRate ? ( + + {t("details.apy_composition.up_to", { + value: totalRateFormatted, + })} + + ) : null} diff --git a/packages/widget/src/components/molecules/select-validator/index.tsx b/packages/widget/src/components/molecules/select-validator/index.tsx index 47acd9bd..aa69bffe 100644 --- a/packages/widget/src/components/molecules/select-validator/index.tsx +++ b/packages/widget/src/components/molecules/select-validator/index.tsx @@ -12,7 +12,7 @@ type SelectValidatorProps = PropsWithChildren< selectedValidators: Set; onItemClick: (item: ValidatorDto) => void; onViewMoreClick?: () => void; - validators: YieldDto["validators"]; + validators: ValidatorDto[]; selectedStake: YieldDto; multiSelect: boolean; } & ( diff --git a/packages/widget/src/config/index.ts b/packages/widget/src/config/index.ts index d8045bd5..0a04004d 100644 --- a/packages/widget/src/config/index.ts +++ b/packages/widget/src/config/index.ts @@ -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, diff --git a/packages/widget/src/domain/index.ts b/packages/widget/src/domain/index.ts index d1cc1834..095ea757 100644 --- a/packages/widget/src/domain/index.ts +++ b/packages/widget/src/domain/index.ts @@ -1,6 +1,5 @@ import type { ActionDto, - PendingActionDto, TokenDto, TransactionDto, TransactionStatus, @@ -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 & { + 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 = ({ @@ -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); diff --git a/packages/widget/src/domain/types/pending-action.ts b/packages/widget/src/domain/types/pending-action.ts new file mode 100644 index 00000000..e8a3fbbd --- /dev/null +++ b/packages/widget/src/domain/types/pending-action.ts @@ -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, + }; +}; + +const getPendingActionArgument = ( + pendingAction: AnyPendingActionDto, + name: PendingActionArgName +) => { + const v2Field = ( + pendingAction as YieldPendingActionDto + ).arguments?.fields?.find( + ( + field: NonNullable["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; +}; diff --git a/packages/widget/src/domain/types/positions.ts b/packages/widget/src/domain/types/positions.ts index 0bfb385a..c9c9a5a0 100644 --- a/packages/widget/src/domain/types/positions.ts +++ b/packages/widget/src/domain/types/positions.ts @@ -1,14 +1,16 @@ +import type { TokenDto } from "@stakekit/api-hooks"; +import BigNumber from "bignumber.js"; import type { - BalanceTypes, - TokenDto, YieldBalanceDto, - YieldBalancesWithIntegrationIdDto, -} from "@stakekit/api-hooks"; -import BigNumber from "bignumber.js"; + YieldBalancesByYieldDto, + YieldBalanceType, + YieldRewardRateDto, +} from "../../providers/yield-api-client-provider/types"; +import type { components } from "../../types/yield-api-schema"; import { equalTokens } from ".."; export type PositionBalancesByType = Map< - BalanceTypes, + YieldBalanceType, (YieldBalanceDto & { tokenPriceInUsd: BigNumber; })[] @@ -16,12 +18,19 @@ export type PositionBalancesByType = Map< export type PositionDetailsLabelType = "hasFrozenV1"; +type BalanceType = "validators" | "default"; + +export type BalanceDataKey = + | BalanceType + | `validator::${components["schemas"]["ValidatorDto"]["address"]}`; + export type PositionsData = Map< - YieldBalancesWithIntegrationIdDto["integrationId"], + YieldBalancesByYieldDto["yieldId"], { - integrationId: YieldBalancesWithIntegrationIdDto["integrationId"]; + yieldId: YieldBalancesByYieldDto["yieldId"]; + rewardRate?: YieldRewardRateDto | null; balanceData: Map< - YieldBalanceDto["groupId"], + BalanceDataKey, { balances: YieldBalanceDto[] } & ( | { type: "validators"; validatorsAddresses: string[] } | { type: "default" } @@ -30,22 +39,64 @@ export type PositionsData = Map< } >; -export const getPositionTotalAmount = ({ - token, - balances, -}: { - token: TokenDto & { pricePerShare: YieldBalanceDto["pricePerShare"] }; - balances: YieldBalanceDto[]; -}) => - balances.reduce((acc, b) => { - if (b.token.isPoints) return acc; - - if (equalTokens(b.token, token)) { - return BigNumber(b.amount).plus(acc); - } - - return BigNumber(b.amount) - .times(b.pricePerShare) - .dividedBy(token.pricePerShare) - .plus(acc); - }, new BigNumber(0)); +export const getPositionBalanceDataKey = ( + balance: YieldBalanceDto +): BalanceDataKey => { + if (Array.isArray(balance.validators) && balance.validators.length > 1) { + return "validators"; + } + + if (balance.validator?.address) { + return `validator::${balance.validator.address}` as BalanceDataKey; + } + + return "default"; +}; + +export const getPositionTotalAmount = ( + balances: YieldBalanceDto[], + baseToken: TokenDto +) => { + const baseTokenBalance = balances.find((b) => + equalTokens(b.token, baseToken) + ); + + const baseTokenPriceInUsd = (() => { + if (!baseTokenBalance?.amountUsd) return null; + + const amount = BigNumber(baseTokenBalance.amount); + if (amount.lte(0)) return null; + + return BigNumber(baseTokenBalance.amountUsd).dividedBy(amount); + })(); + + return balances.reduce( + (acc, b) => { + if (b.token.isPoints) return acc; + + if (baseTokenBalance && equalTokens(b.token, baseTokenBalance.token)) { + return { + amount: acc.amount.plus(b.amount), + amountUsd: acc.amountUsd.plus(b.amountUsd ?? 0), + }; + } + + const balanceAmountUsd = BigNumber(b.amountUsd ?? 0); + + if (baseTokenPriceInUsd && !baseTokenPriceInUsd.isZero()) { + return { + amount: acc.amount.plus( + balanceAmountUsd.dividedBy(baseTokenPriceInUsd) + ), + amountUsd: acc.amountUsd.plus(balanceAmountUsd), + }; + } + + return { + amount: acc.amount.plus(b.amount), + amountUsd: acc.amountUsd.plus(balanceAmountUsd), + }; + }, + { amount: new BigNumber(0), amountUsd: new BigNumber(0) } + ); +}; diff --git a/packages/widget/src/domain/types/price.ts b/packages/widget/src/domain/types/price.ts index b63a7433..76494684 100644 --- a/packages/widget/src/domain/types/price.ts +++ b/packages/widget/src/domain/types/price.ts @@ -1,9 +1,14 @@ -import type { TokenDto } from "@stakekit/api-hooks"; import BigNumber from "bignumber.js"; import { Maybe } from "purify-ts"; import { tokenString } from ".."; import type { TokenString } from "./tokens"; +type PriceToken = { + symbol: string; + network: string; + address?: string; +}; + export type Price = { price: number | undefined; price24H: number | undefined; @@ -12,7 +17,7 @@ export type Price = { export class Prices { constructor(public value: Map) {} - getByToken(token: TokenDto) { + getByToken(token: PriceToken) { return Maybe.fromNullable(this.value.get(tokenString(token))); } } @@ -24,8 +29,8 @@ export const getTokenPriceInUSD = ({ prices, pricePerShare, }: { - token: TokenDto; - baseToken: TokenDto | null; + token: PriceToken; + baseToken: PriceToken | null; amount: string | BigNumber; pricePerShare: string | null; prices: Prices; diff --git a/packages/widget/src/domain/types/reward-rate.ts b/packages/widget/src/domain/types/reward-rate.ts new file mode 100644 index 00000000..29784e65 --- /dev/null +++ b/packages/widget/src/domain/types/reward-rate.ts @@ -0,0 +1,86 @@ +import type { RewardTypes, YieldDto } from "@stakekit/api-hooks"; +import type { + YieldRewardDto, + YieldRewardRateDto, +} from "../../providers/yield-api-client-provider/types"; + +type YieldDtoWithRewardRateDetails = YieldDto & { + rewardRateDetails?: YieldRewardRateDto; +}; + +export type RewardRateBreakdownKey = + | "native" + | "protocol_incentive" + | "campaign"; + +export type RewardRateBreakdownItem = { + key: RewardRateBreakdownKey; + rate: number; + rewardType: RewardTypes; + isUpTo: boolean; +}; + +const breakdownOrder: RewardRateBreakdownKey[] = [ + "native", + "protocol_incentive", + "campaign", +]; + +export const getRewardTypeFromRateType = ( + rateType: string | null | undefined +): RewardTypes => { + const normalized = rateType?.toLowerCase(); + + if (normalized === "apr" || normalized === "apy") { + return normalized; + } + + return "variable"; +}; + +const getBreakdownKey = ( + yieldSource: YieldRewardDto["yieldSource"] +): RewardRateBreakdownKey => + yieldSource === "campaign_incentive" + ? "campaign" + : yieldSource === "protocol_incentive" + ? "protocol_incentive" + : "native"; + +export const getYieldRewardRateDetails = ( + yieldDto: YieldDto | null | undefined +): YieldRewardRateDto | undefined => + (yieldDto as YieldDtoWithRewardRateDetails | null | undefined) + ?.rewardRateDetails; + +export const getRewardRateBreakdown = ( + rewardRate: YieldRewardRateDto | null | undefined, + opts?: { + showUpToCampaign?: boolean; + } +): RewardRateBreakdownItem[] => { + if (!rewardRate?.components?.length) { + return []; + } + + const buckets = rewardRate.components.reduce((acc, component) => { + const key = getBreakdownKey(component.yieldSource); + const prev = acc.get(key); + + acc.set(key, { + key, + rate: (prev?.rate ?? 0) + component.rate, + rewardType: + prev?.rewardType ?? getRewardTypeFromRateType(component.rateType), + isUpTo: key === "campaign" && !!opts?.showUpToCampaign, + }); + + return acc; + }, new Map()); + + return breakdownOrder.flatMap((key) => { + const item = buckets.get(key); + + return item && item.rate > 0 ? [item] : []; + }); +}; diff --git a/packages/widget/src/domain/types/stake.ts b/packages/widget/src/domain/types/stake.ts index 115a5b71..0f5a0fb7 100644 --- a/packages/widget/src/domain/types/stake.ts +++ b/packages/widget/src/domain/types/stake.ts @@ -1,6 +1,7 @@ import type { AmountArgumentOptionsDto, TokenBalanceScanResponseDto, + ValidatorDto, YieldDto, } from "@stakekit/api-hooks"; import { Networks } from "@stakekit/common"; @@ -126,7 +127,7 @@ const balanceValidForYield = ({ export const getInitSelectedValidators = (args: { initQueryParams: Maybe; - yieldDto: YieldDto; + validators: ValidatorDto[]; }) => args.initQueryParams .chainNullable((params) => params.validator) @@ -135,10 +136,10 @@ export const getInitSelectedValidators = (args: { (val) => val.name?.toLowerCase() === initV.toLowerCase() || val.address === initV, - args.yieldDto.validators + args.validators ) ) - .altLazy(() => List.head(args.yieldDto.validators)) + .altLazy(() => List.head(args.validators)) .map((v) => new Map([[v.address, v]])) .orDefault(new Map()); @@ -174,7 +175,7 @@ export const getMinStakeAmount = ( const hasStaked = Maybe.fromNullable(positionsData.get(yieldDto.id)) .map((val) => [...val.balanceData.values()]) .map((val) => - val.some((v) => v.balances.some((b) => b.type === "staked")) + val.some((v) => v.balances.some((b) => b.type === "active")) ) .orDefault(false); diff --git a/packages/widget/src/domain/types/yields.ts b/packages/widget/src/domain/types/yields.ts index e33c0169..1cd9b66a 100644 --- a/packages/widget/src/domain/types/yields.ts +++ b/packages/widget/src/domain/types/yields.ts @@ -1,4 +1,4 @@ -import type { YieldDto, YieldType } from "@stakekit/api-hooks"; +import type { ValidatorDto, YieldDto, YieldType } from "@stakekit/api-hooks"; import { EvmNetworks } from "@stakekit/common"; import BigNumber from "bignumber.js"; import type { TFunction } from "i18next"; @@ -27,45 +27,55 @@ export type ValidatorsConfig = Map< } >; -export const filterMapValidators = ( - validatorsConfig: ValidatorsConfig, - yieldDto: YieldDto -): YieldDto => { +export const filterValidators = ({ + validatorsConfig, + validators, + network, + yieldId, +}: { + validatorsConfig: ValidatorsConfig; + validators: ValidatorDto[]; + network: YieldDto["token"]["network"]; + yieldId?: YieldDto["id"]; +}) => { const valConfig = Maybe.fromNullable( - validatorsConfig.get(yieldDto.token.network as SupportedSKChains) + validatorsConfig.get(network as SupportedSKChains) ) .altLazy(() => Maybe.fromNullable(validatorsConfig.get("*"))) .extractNullable(); - if (!valConfig) { - return yieldDto; + const filtered = !valConfig + ? validators + : (() => { + const { + allowed, + blocked, + preferred, + mergePreferredWithDefault, + preferredOnly, + } = valConfig; + + return validators.flatMap((v) => { + if (allowed && !allowed.has(v.address)) return []; + if (blocked?.has(v.address)) return []; + + const isPreferred = + preferred?.has(v.address) || + !!(mergePreferredWithDefault && v.preferred); + + if (preferredOnly) { + return isPreferred ? [{ ...v, preferred: true }] : []; + } + + return [{ ...v, preferred: isPreferred }]; + }); + })(); + + if (yieldId && isBittensorStaking(yieldId)) { + return filtered.filter((validator) => validator.name?.match(/yuma/i)); } - const { - allowed, - blocked, - preferred, - mergePreferredWithDefault, - preferredOnly, - } = valConfig; - - return { - ...yieldDto, - validators: yieldDto.validators.flatMap((v) => { - if (allowed && !allowed.has(v.address)) return []; - if (blocked?.has(v.address)) return []; - - const isPreferred = - preferred?.has(v.address) || - !!(mergePreferredWithDefault && v.preferred); - - if (preferredOnly) { - return isPreferred ? [{ ...v, preferred: true }] : []; - } - - return [{ ...v, preferred: isPreferred }]; - }), - }; + return filtered; }; export const getExtendedYieldType = (yieldDto: YieldDto) => diff --git a/packages/widget/src/hooks/api/use-activity-actions.ts b/packages/widget/src/hooks/api/use-activity-actions.ts index b577c9a2..7aa74c46 100644 --- a/packages/widget/src/hooks/api/use-activity-actions.ts +++ b/packages/widget/src/hooks/api/use-activity-actions.ts @@ -1,58 +1,57 @@ -import { - type ActionDto, - type ActionList200, - ActionStatus, - actionList, - getActionListQueryKey, -} from "@stakekit/api-hooks"; +import { type ActionDto, ActionStatus } from "@stakekit/api-hooks"; import { useInfiniteQuery } from "@tanstack/react-query"; import { EitherAsync } from "purify-ts"; import { useMemo } from "react"; import { useSKQueryClient } from "../../providers/query-client"; import { useSKWallet } from "../../providers/sk-wallet"; -import { useValidatorsConfig } from "../use-validators-config"; +import { useYieldApiFetchClient } from "../../providers/yield-api-client-provider"; +import { listActions } from "../../providers/yield-api-client-provider/actions"; +import { adaptActionDto } from "../../providers/yield-api-client-provider/compat"; import { getYieldOpportunity } from "./use-yield-opportunity/get-yield-opportunity"; +const PAGE_SIZE = 20; + export const useActivityActions = () => { - const { address, network, isLedgerLive } = useSKWallet(); + const { address, isLedgerLive } = useSKWallet(); const queryClient = useSKQueryClient(); - - const validatorsConfig = useValidatorsConfig(); + const yieldApiFetchClient = useYieldApiFetchClient(); const query = useInfiniteQuery({ - enabled: !!address && !!network, - queryKey: getActionListQueryKey({ - network: network!, - walletAddress: address!, - }), - queryFn: async ({ pageParam = 1 }) => { + enabled: !!address, + queryKey: ["activity-actions", address], + queryFn: async ({ pageParam = 0 }) => { return ( await EitherAsync(() => - actionList({ - page: pageParam, - walletAddress: address!, - network: network!, - sort: "createdAtDesc", + listActions({ + address: address!, + fetchClient: yieldApiFetchClient, + limit: PAGE_SIZE, + offset: pageParam, }) ) .mapLeft(() => new Error("Could not get action list")) .map((actionList) => ({ ...actionList, - data: actionList.data.filter( + data: (actionList.items ?? []).filter( (x) => x.status !== ActionStatus.CREATED ), })) .chain(async (actionList) => EitherAsync.all( - (actionList.data as ActionList200["data"]).map((action) => + actionList.data.map((action) => getYieldOpportunity({ - yieldId: action.integrationId, + yieldId: action.yieldId, queryClient, isLedgerLive, - validatorsConfig, + yieldApiFetchClient, }) .map((yieldData) => ({ - actionData: action as typeof action & ActionDto, + actionData: adaptActionDto({ + actionDto: action, + addresses: { address: action.address }, + gasFeeToken: yieldData.metadata.gasFeeToken, + yieldDto: yieldData, + }) as ActionDto, yieldData, })) .chainLeft(() => EitherAsync(() => Promise.resolve(null))) @@ -64,9 +63,10 @@ export const useActivityActions = () => { ) ).unsafeCoerce(); }, - initialPageParam: 1, + initialPageParam: 0, getNextPageParam: (lastPage) => { - return lastPage.hasNextPage ? lastPage.page + 1 : undefined; + const nextOffset = (lastPage.offset ?? 0) + (lastPage.limit ?? PAGE_SIZE); + return nextOffset < (lastPage.total ?? 0) ? nextOffset : undefined; }, }); diff --git a/packages/widget/src/hooks/api/use-multi-yields.ts b/packages/widget/src/hooks/api/use-multi-yields.ts index b2b263bb..dd99de4b 100644 --- a/packages/widget/src/hooks/api/use-multi-yields.ts +++ b/packages/widget/src/hooks/api/use-multi-yields.ts @@ -37,6 +37,7 @@ import { } from "../../domain/types/yields"; import { useSKQueryClient } from "../../providers/query-client"; import { useSKWallet } from "../../providers/sk-wallet"; +import { useYieldApiFetchClient } from "../../providers/yield-api-client-provider"; import { useSavedRef } from "../use-saved-ref"; import { useValidatorsConfig } from "../use-validators-config"; import { getYieldOpportunity } from "./use-yield-opportunity/get-yield-opportunity"; @@ -61,10 +62,12 @@ const multiYieldsStore = createStore({ export const useStreamMultiYields = (yieldIds: string[]) => { const { network, isConnected, isLedgerLive } = useSKWallet(); + const yieldApiFetchClient = useYieldApiFetchClient(); const argsRef = useSavedRef({ isLedgerLive, queryClient: useSKQueryClient(), + yieldApiFetchClient, network, isConnected, }); @@ -106,12 +109,14 @@ export const useMultiYields = ( } ) => { const { network, isConnected, isLedgerLive } = useSKWallet(); + const yieldApiFetchClient = useYieldApiFetchClient(); const validatorsConfig = useValidatorsConfig(); const argsRef = useSavedRef({ isLedgerLive, queryClient: useSKQueryClient(), + yieldApiFetchClient, network, isConnected, }); @@ -147,6 +152,7 @@ export const getFirstEligibleYield = ( const multipleYields$ = (args: { isLedgerLive: boolean; queryClient: QueryClient; + yieldApiFetchClient: ReturnType; isConnected: boolean; network: SKWallet["network"]; yieldIds: string[]; @@ -159,7 +165,7 @@ const multipleYields$ = (args: { isLedgerLive: args.isLedgerLive, yieldId: v, queryClient: args.queryClient, - validatorsConfig: args.validatorsConfig, + yieldApiFetchClient: args.yieldApiFetchClient, }) ) ) @@ -182,6 +188,7 @@ const multipleYields$ = (args: { const firstEligibleYield$ = (args: { isLedgerLive: boolean; queryClient: QueryClient; + yieldApiFetchClient: ReturnType; isConnected: boolean; network: SKWallet["network"]; yieldIds: string[]; diff --git a/packages/widget/src/hooks/api/use-prices.ts b/packages/widget/src/hooks/api/use-prices.ts index b048feec..f9942941 100644 --- a/packages/widget/src/hooks/api/use-prices.ts +++ b/packages/widget/src/hooks/api/use-prices.ts @@ -3,6 +3,7 @@ import { useTokenGetTokenPrices } from "@stakekit/api-hooks"; import { useCallback } from "react"; import { createSelector } from "reselect"; import type { Prices } from "../../domain/types/price"; +import type { YieldTokenDto } from "../../providers/yield-api-client-provider/types"; import { priceResponseDtoToPrices } from "../../utils/mappers"; const defaultParam: PriceRequestDto = { @@ -17,14 +18,29 @@ const pricesSelector = createSelector( (val) => priceResponseDtoToPrices(val) ); +type PriceRequestInput = Omit & { + tokenList: (PriceRequestDto["tokenList"][number] | YieldTokenDto)[]; +}; + export const usePrices = ( - priceRequestDto: PriceRequestDto | null | undefined, + priceRequestDto: PriceRequestInput | null | undefined, opts?: { enabled?: boolean; select?: (val: Prices) => T; } ) => { - return useTokenGetTokenPrices(priceRequestDto ?? defaultParam, { + const requestDto = priceRequestDto + ? ({ + ...priceRequestDto, + tokenList: priceRequestDto.tokenList.map((token) => ({ + ...token, + network: + token.network as PriceRequestDto["tokenList"][number]["network"], + })), + } satisfies PriceRequestDto) + : defaultParam; + + return useTokenGetTokenPrices(requestDto, { query: { enabled: !!priceRequestDto && opts?.enabled, select: useCallback( diff --git a/packages/widget/src/hooks/api/use-tokens-prices.ts b/packages/widget/src/hooks/api/use-tokens-prices.ts index 43a716be..ff0fb803 100644 --- a/packages/widget/src/hooks/api/use-tokens-prices.ts +++ b/packages/widget/src/hooks/api/use-tokens-prices.ts @@ -1,7 +1,8 @@ -import type { PriceRequestDto, TokenDto, YieldDto } from "@stakekit/api-hooks"; +import type { TokenDto, YieldDto } from "@stakekit/api-hooks"; import { Maybe } from "purify-ts"; import { useMemo } from "react"; import { config } from "../../config"; +import type { YieldTokenDto } from "../../providers/yield-api-client-provider/types"; import { useBaseToken } from "../use-base-token"; import { useGasFeeToken } from "../use-gas-fee-token"; import { usePrices } from "./use-prices"; @@ -13,7 +14,7 @@ export const useTokensPrices = ({ token, yieldDto, }: { - token: Maybe; + token: Maybe; yieldDto: Maybe; }) => { const baseToken = useBaseToken(yieldDto); @@ -22,7 +23,7 @@ export const useTokensPrices = ({ const priceRequestDto = useMemo( () => Maybe.fromRecord({ baseToken, gasFeeToken, token }) - .map((val) => ({ + .map((val) => ({ currency: config.currency, tokenList: [val.token, val.baseToken, val.gasFeeToken], })) diff --git a/packages/widget/src/hooks/api/use-yield-balances-scan.ts b/packages/widget/src/hooks/api/use-yield-balances-scan.ts index eed36cef..a9063875 100644 --- a/packages/widget/src/hooks/api/use-yield-balances-scan.ts +++ b/packages/widget/src/hooks/api/use-yield-balances-scan.ts @@ -1,22 +1,23 @@ -import type { - YieldBalanceScanRequestDto, - YieldBalancesWithIntegrationIdDto, -} from "@stakekit/api-hooks"; -import { useQuery } from "@tanstack/react-query"; -import { Just, Maybe } from "purify-ts"; +import type { UseQueryResult } from "@tanstack/react-query"; +import { Maybe } from "purify-ts"; import { useCallback, useMemo } from "react"; -import { yieldYieldBalancesScan } from "../../common/private-api"; import { useSKQueryClient } from "../../providers/query-client"; import { useSKWallet } from "../../providers/sk-wallet"; import { useActionHistoryData } from "../../providers/stake-history"; +import { useYieldApiClient } from "../../providers/yield-api-client-provider"; +import type { + YieldBalanceDto, + YieldBalancesByYieldDto, + YieldBalancesRequestDto, +} from "../../providers/yield-api-client-provider/types"; import { useInvalidateQueryNTimes } from "../use-invalidate-query-n-times"; -export const useYieldBalancesScan = < - T = YieldBalancesWithIntegrationIdDto[], ->(opts?: { - select?: (data: YieldBalancesWithIntegrationIdDto[]) => T; -}) => { - const { network, address, additionalAddresses } = useSKWallet(); +export const useYieldBalancesScan = (opts?: { + select?: (data: YieldBalancesByYieldDto[]) => T; + // biome-ignore lint/suspicious/noExplicitAny: fix later +}): UseQueryResult => { + const yieldApi = useYieldApiClient(); + const { network, address } = useSKWallet(); const actionHistoryData = useActionHistoryData(); @@ -28,38 +29,51 @@ export const useYieldBalancesScan = < const param = useMemo( () => Maybe.fromRecord({ - additionalAddresses: Just(additionalAddresses ?? undefined), address: Maybe.fromNullable(address), network: Maybe.fromNullable(network), - }).mapOrDefault<{ dto: YieldBalanceScanRequestDto; enabled: boolean }>( + }).mapOrDefault<{ dto: YieldBalancesRequestDto; enabled: boolean }>( (val) => ({ enabled: true, dto: { - addresses: { - address: val.address, - additionalAddresses: val.additionalAddresses, - }, - network: val.network, + queries: [ + { + address: val.address, + network: + val.network as YieldBalancesRequestDto["queries"][number]["network"], + }, + ], }, }), { enabled: false, dto: { - addresses: { address: "", additionalAddresses: undefined }, - network: "ethereum", + queries: [{ address: "", network: "ethereum" }], }, } ), - [additionalAddresses, address, network] + [address, network] ); - const res = useQuery({ - queryKey: getYieldYieldBalancesScanQueryKey(param.dto), - queryFn: () => yieldYieldBalancesScan(param.dto), - enabled: param.enabled, - select: opts?.select, - refetchInterval: 1000 * 60, - }); + const res = yieldApi.useQuery( + "post", + "/v1/yields/balances", + { + body: param.dto, + }, + { + enabled: param.enabled, + refetchInterval: 1000 * 60, + select: (data) => { + const items = data.items as YieldBalancesByYieldDto[]; + + if (opts?.select) { + return opts.select(items); + } + + return items as T; + }, + } + ); /** * This is a hack to make sure that the yield balances are updated after a transaction @@ -67,7 +81,7 @@ export const useYieldBalancesScan = < useInvalidateQueryNTimes({ enabled: !!lastActionTimestamp, key: ["yield-balances-refetch", lastActionTimestamp], - queryKey: [getYieldYieldBalancesScanQueryKey(param.dto)[0]], + queryKey: getYieldYieldBalancesScanQueryKey(), waitMs: 4000, shouldRefetch: () => !!lastActionTimestamp && Date.now() - lastActionTimestamp < 1000 * 12, @@ -82,18 +96,29 @@ export const useInvalidateYieldBalances = () => { return useCallback( () => queryClient.invalidateQueries({ - queryKey: [ - getYieldYieldBalancesScanQueryKey( - {} as YieldBalanceScanRequestDto - )[0], - ], + queryKey: getYieldYieldBalancesScanQueryKey(), }), [queryClient] ); }; -const getYieldYieldBalancesScanQueryKey = ( - yieldBalanceScanRequestDto: YieldBalanceScanRequestDto -) => { - return ["/v1/yields/balances/scan", yieldBalanceScanRequestDto] as const; -}; +const getYieldYieldBalancesScanQueryKey = () => + ["post", "/v1/yields/balances"] as const; + +const normalizeYieldBalanceForPosition = ( + balance: YieldBalanceDto +): YieldBalanceDto => ({ + ...balance, + amount: balance.shareAmount ?? balance.amount, +}); + +export const normalizeYieldBalancesForPosition = ( + balances: YieldBalancesByYieldDto[] +): YieldBalancesByYieldDto[] => + balances.map((balanceByYield) => ({ + ...balanceByYield, + balances: balanceByYield.balances.map(normalizeYieldBalanceForPosition), + outputTokenBalance: balanceByYield.outputTokenBalance + ? normalizeYieldBalanceForPosition(balanceByYield.outputTokenBalance) + : balanceByYield.outputTokenBalance, + })); diff --git a/packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.ts b/packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.ts index 0c248f47..425c7d59 100644 --- a/packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.ts +++ b/packages/widget/src/hooks/api/use-yield-opportunity/get-yield-opportunity.ts @@ -1,19 +1,17 @@ import type { YieldDto } from "@stakekit/api-hooks"; import type { QueryClient } from "@tanstack/react-query"; +import type { Client } from "openapi-fetch"; import { EitherAsync } from "purify-ts"; import { yieldYieldOpportunity } from "../../../common/private-api"; -import { - filterMapValidators, - getComputedRewardRate, - isBittensorStaking, - isEthenaUsdeStaking, - type ValidatorsConfig, -} from "../../../domain/types/yields"; +import { isEthenaUsdeStaking } from "../../../domain/types/yields"; +import { adaptYieldDto } from "../../../providers/yield-api-client-provider/compat"; +import { getResponseData } from "../../../providers/yield-api-client-provider/request-helpers"; +import type { paths } from "../../../types/yield-api-schema"; type Params = { yieldId: string; isLedgerLive: boolean; - validatorsConfig: ValidatorsConfig; + yieldApiFetchClient: Client; signal?: AbortSignal; }; @@ -50,38 +48,65 @@ const fn = ({ isLedgerLive, yieldId, signal, - validatorsConfig, + yieldApiFetchClient, }: Params & { signal?: AbortSignal; -}) => - EitherAsync(() => - yieldYieldOpportunity( - yieldId, - { - ledgerWalletAPICompatible: isLedgerLive, - }, - signal - ) - ) - .map((y) => filterMapValidators(validatorsConfig, y)) +}) => { + const stripValidators = (yieldDto: YieldDto): YieldDto => ({ + ...yieldDto, + validators: [], + }); + + return EitherAsync(async () => { + const [newYieldResult, legacyYieldResult] = await Promise.allSettled([ + getResponseData( + yieldApiFetchClient.GET("/v1/yields/{yieldId}", { + params: { + path: { + yieldId, + }, + }, + signal, + }) + ), + yieldYieldOpportunity( + yieldId, + { ledgerWalletAPICompatible: isLedgerLive }, + signal + ), + ]); + + if (newYieldResult.status === "rejected") { + if (legacyYieldResult.status === "fulfilled") { + return stripValidators(legacyYieldResult.value); + } + + throw newYieldResult.reason; + } + + const merged = adaptYieldDto({ + yieldDto: newYieldResult.value, + legacyYieldDto: + legacyYieldResult.status === "fulfilled" + ? legacyYieldResult.value + : null, + }); + + return stripValidators(merged); + }) .map((y) => isEthenaUsdeStaking(y.id) ? ({ ...y, - rewardRate: getComputedRewardRate(y), metadata: { ...y.metadata, name: y.metadata.name.replace(/staking/i, ""), }, } satisfies YieldDto) - : isBittensorStaking(y.id) - ? { - ...y, - validators: y.validators.filter((v) => v.name?.match(/yuma/i)), - } - : y + : y ) .mapLeft((e) => { console.log(e); return new Error("Could not get yield opportunity"); }); +}; diff --git a/packages/widget/src/hooks/api/use-yield-opportunity/index.ts b/packages/widget/src/hooks/api/use-yield-opportunity/index.ts index b8a7ba38..4160f0c2 100644 --- a/packages/widget/src/hooks/api/use-yield-opportunity/index.ts +++ b/packages/widget/src/hooks/api/use-yield-opportunity/index.ts @@ -1,13 +1,12 @@ import { useQuery } from "@tanstack/react-query"; -import type { ValidatorsConfig } from "../../../domain/types/yields"; import { useSKWallet } from "../../../providers/sk-wallet"; -import { useValidatorsConfig } from "../../use-validators-config"; +import { useYieldApiFetchClient } from "../../../providers/yield-api-client-provider"; import { queryFn } from "./get-yield-opportunity"; type Params = { yieldId: string; isLedgerLive: boolean; - validatorsConfig: ValidatorsConfig; + yieldApiFetchClient: ReturnType; signal?: AbortSignal; }; @@ -20,16 +19,24 @@ const getKey = (params: Params) => [ export const useYieldOpportunity = (integrationId: string | undefined) => { const { isLedgerLive } = useSKWallet(); - - const validatorsConfig = useValidatorsConfig(); + const yieldApiFetchClient = useYieldApiFetchClient(); const yieldId = integrationId ?? ""; return useQuery({ - queryKey: getKey({ yieldId, isLedgerLive, validatorsConfig }), + queryKey: getKey({ + yieldId, + isLedgerLive, + yieldApiFetchClient, + }), enabled: !!integrationId, staleTime, queryFn: ({ signal }) => - queryFn({ yieldId, isLedgerLive, signal, validatorsConfig }), + queryFn({ + yieldId, + isLedgerLive, + signal, + yieldApiFetchClient, + }), }); }; diff --git a/packages/widget/src/hooks/api/use-yield-validators.ts b/packages/widget/src/hooks/api/use-yield-validators.ts new file mode 100644 index 00000000..7a19a8af --- /dev/null +++ b/packages/widget/src/hooks/api/use-yield-validators.ts @@ -0,0 +1,104 @@ +import type { ValidatorDto, YieldDto } from "@stakekit/api-hooks"; +import { useQuery } from "@tanstack/react-query"; +import type { ValidatorsConfig } from "../../domain/types/yields"; +import { filterValidators } from "../../domain/types/yields"; +import { useYieldApiFetchClient } from "../../providers/yield-api-client-provider"; +import { adaptValidatorDto } from "../../providers/yield-api-client-provider/compat"; +import { getResponseData } from "../../providers/yield-api-client-provider/request-helpers"; +import { useValidatorsConfig } from "../use-validators-config"; + +const PAGE_SIZE = 100; +const staleTime = 1000 * 60 * 2; + +type Params = { + yieldId: string; + network?: YieldDto["token"]["network"]; + validatorsConfig: ValidatorsConfig; + yieldApiFetchClient: ReturnType; + signal?: AbortSignal; +}; + +const getYieldValidatorsQueryKey = ({ yieldId }: Pick) => [ + "yield-validators", + yieldId, +]; + +export const getYieldValidatorsQueryFn = async ({ + yieldId, + network, + validatorsConfig, + yieldApiFetchClient, + signal, +}: Params): Promise => { + const fetchPage = (offset: number) => + getResponseData( + yieldApiFetchClient.GET("/v1/yields/{yieldId}/validators", { + params: { + path: { + yieldId, + }, + query: { + offset, + limit: PAGE_SIZE, + }, + }, + signal, + }) + ); + + const firstPage = await fetchPage(0); + + const remainingOffsets = Array.from( + { length: Math.ceil(firstPage.total / PAGE_SIZE) - 1 }, + (_, index) => (index + 1) * PAGE_SIZE + ); + + const remainingPages = await Promise.all( + remainingOffsets.map((offset) => + fetchPage(offset).catch(() => ({ items: [] })) + ) + ); + + const validators = [firstPage, ...remainingPages] + .flatMap((page) => page.items ?? []) + .map(adaptValidatorDto); + + return network + ? filterValidators({ + validatorsConfig, + validators, + network, + yieldId, + }) + : validators; +}; + +const getYieldValidatorsQueryOptions = (params: Params) => ({ + queryKey: getYieldValidatorsQueryKey(params), + staleTime, + queryFn: ({ signal }: { signal?: AbortSignal }) => + getYieldValidatorsQueryFn({ ...params, signal }), +}); + +export const useYieldValidators = ({ + enabled = true, + yieldId, + network, +}: { + enabled?: boolean; + yieldId?: string; + network?: YieldDto["token"]["network"]; +}) => { + const yieldApiFetchClient = useYieldApiFetchClient(); + const validatorsConfig = useValidatorsConfig(); + + return useQuery({ + ...getYieldValidatorsQueryOptions({ + yieldId: yieldId ?? "", + network, + validatorsConfig, + yieldApiFetchClient, + }), + enabled: enabled && !!yieldId, + }); +}; diff --git a/packages/widget/src/hooks/navigation/use-unstake-or-pending-action-params.ts b/packages/widget/src/hooks/navigation/use-unstake-or-pending-action-params.ts index e17ea5dd..970bfd6e 100644 --- a/packages/widget/src/hooks/navigation/use-unstake-or-pending-action-params.ts +++ b/packages/widget/src/hooks/navigation/use-unstake-or-pending-action-params.ts @@ -1,6 +1,6 @@ -import type { ActionTypes } from "@stakekit/api-hooks"; import { Maybe } from "purify-ts"; import { useMemo } from "react"; +import type { YieldPendingActionType } from "../../providers/yield-api-client-provider/types"; import { usePendingActionSelectValidatorMatch } from "./use-pending-action-select-validator-match"; import { useUnstakeOrPendingActionMatch } from "./use-unstake-or-pending-action-match"; @@ -16,7 +16,7 @@ export const useUnstakeOrPendingActionParams = () => { {}; const pendingActionType = pendingActionSelectValidatorMatch?.params - .pendingActionType as ActionTypes | undefined; + .pendingActionType as YieldPendingActionType | undefined; return { balanceId: Maybe.fromNullable(balanceId), diff --git a/packages/widget/src/hooks/use-geo-block.ts b/packages/widget/src/hooks/use-geo-block.ts index 6b55e5df..f18ceb6a 100644 --- a/packages/widget/src/hooks/use-geo-block.ts +++ b/packages/widget/src/hooks/use-geo-block.ts @@ -19,23 +19,39 @@ const subscribe = (callback: (val: typeof _isGeoBlocked) => void) => { return () => subs.delete(callback); }; +const isGeoLocationError = (data: unknown): data is GeolocationError => + typeof data === "object" && + data !== null && + "type" in data && + data.type === GeolocationErrorType.GEO_LOCATION; + +export const handleGeoBlockResponse = ({ + data, + status, +}: { + data: unknown; + status?: number; +}) => { + if (status !== 403 || !isGeoLocationError(data)) { + return; + } + + const regionCode = (data.regionCode as unknown as string) ?? ""; // wrong type in API + + _isGeoBlocked = { + tags: new Set(data.tags ?? []), + countryCode: data.countryCode ?? "", + regionCode, + }; + notify(); +}; + export const attachGeoBlockInterceptor = (apiClient: AxiosInstance) => apiClient.interceptors.response.use(undefined, (error) => { - if ( - error?.response?.status === 403 && - error.response.data?.type === GeolocationErrorType.GEO_LOCATION - ) { - const geoLocationErr = error.response.data as GeolocationError; - - const regionCode = (geoLocationErr.regionCode as unknown as string) ?? ""; // wrong type in API - - _isGeoBlocked = { - tags: new Set(geoLocationErr.tags ?? []), - countryCode: geoLocationErr.countryCode ?? "", - regionCode, - }; - notify(); - } + handleGeoBlockResponse({ + data: error?.response?.data, + status: error?.response?.status, + }); return Promise.reject(error); }); diff --git a/packages/widget/src/hooks/use-handle-deep-links.ts b/packages/widget/src/hooks/use-handle-deep-links.ts index 4ab9dfc2..82b1ce27 100644 --- a/packages/widget/src/hooks/use-handle-deep-links.ts +++ b/packages/widget/src/hooks/use-handle-deep-links.ts @@ -63,7 +63,7 @@ export const useHandleDeepLinks = () => { gasFeeToken: val.pendingActionDto.gasFeeToken, integrationData: val.pendingActionDto.integrationData, interactedToken: val.balance.token, - pendingActionType: val.pendingActionDto.requestDto.type, + pendingActionType: val.pendingActionDto.requestDto.action, }, }); navigateRef.current( diff --git a/packages/widget/src/hooks/use-init-params.ts b/packages/widget/src/hooks/use-init-params.ts index 449a02cb..db7f1147 100644 --- a/packages/widget/src/hooks/use-init-params.ts +++ b/packages/widget/src/hooks/use-init-params.ts @@ -3,14 +3,13 @@ import { useQuery } from "@tanstack/react-query"; import { EitherAsync, Right } from "purify-ts"; import type { SupportedSKChains } from "../domain/types/chains"; import type { InitParams } from "../domain/types/init-params"; -import type { ValidatorsConfig } from "../domain/types/yields"; import { useSKQueryClient } from "../providers/query-client"; import { useSettings } from "../providers/settings"; import type { SettingsContextType } from "../providers/settings/types"; import { useSKWallet } from "../providers/sk-wallet"; +import { useYieldApiFetchClient } from "../providers/yield-api-client-provider"; import { getYieldOpportunity } from "./api/use-yield-opportunity/get-yield-opportunity"; import { getAndValidateInitParams } from "./use-init-query-params"; -import { useValidatorsConfig } from "./use-validators-config"; const queryKey = ["init-params"]; const staleTime = 0; @@ -22,7 +21,7 @@ export const useInitParams = (opts?: { const { isLedgerLive } = useSKWallet(); const { externalProviders } = useSettings(); const queryClient = useSKQueryClient(); - const validatorsConfig = useValidatorsConfig(); + const yieldApiFetchClient = useYieldApiFetchClient(); return useQuery({ queryKey, @@ -32,8 +31,8 @@ export const useInitParams = (opts?: { queryFn({ isLedgerLive, queryClient, + yieldApiFetchClient, externalProviders, - validatorsConfig, }), select: opts?.select, }); @@ -60,13 +59,13 @@ const queryFn = async (params: Parameters[0]) => const fn = ({ isLedgerLive, queryClient, + yieldApiFetchClient, externalProviders, - validatorsConfig, }: { isLedgerLive: boolean; queryClient: QueryClient; + yieldApiFetchClient: ReturnType; externalProviders: SettingsContextType["externalProviders"]; - validatorsConfig: ValidatorsConfig; }): EitherAsync => EitherAsync.liftEither( getAndValidateInitParams({ @@ -80,7 +79,7 @@ const fn = ({ isLedgerLive, yieldId: yId, queryClient, - validatorsConfig, + yieldApiFetchClient, }) .map((yieldData) => ({ ...val, diff --git a/packages/widget/src/hooks/use-init-query-params.ts b/packages/widget/src/hooks/use-init-query-params.ts index 7f02e4b1..8174f161 100644 --- a/packages/widget/src/hooks/use-init-query-params.ts +++ b/packages/widget/src/hooks/use-init-query-params.ts @@ -1,4 +1,3 @@ -import { ActionTypes } from "@stakekit/api-hooks"; import { Codec, Left, Right, string } from "purify-ts"; import { useMemo } from "react"; import { @@ -7,6 +6,7 @@ import { } from "../domain/types/chains"; import type { TokenString } from "../domain/types/tokens"; import { useSettings } from "../providers/settings"; +import type { YieldPendingActionType } from "../providers/yield-api-client-provider/types"; import { MaybeWindow } from "../utils/maybe-window"; export const useInitQueryParams = () => { @@ -21,13 +21,13 @@ export const useInitQueryParams = () => { ); }; -const pendingActionCodec = Codec.custom({ +const pendingActionCodec = Codec.custom({ decode: (val) => string .decode(val) .chain((v) => - v in ActionTypes - ? Right(v as ActionTypes) + /^[A-Z_]+$/.test(v) + ? Right(v as YieldPendingActionType) : Left("invalid pending action") ), encode: (val) => val, diff --git a/packages/widget/src/hooks/use-position-balance-by-type.ts b/packages/widget/src/hooks/use-position-balance-by-type.ts index 5a9967db..cb7e7af4 100644 --- a/packages/widget/src/hooks/use-position-balance-by-type.ts +++ b/packages/widget/src/hooks/use-position-balance-by-type.ts @@ -1,67 +1,45 @@ -import type { TokenDto, YieldBalanceDto } from "@stakekit/api-hooks"; import BigNumber from "bignumber.js"; -import { Maybe } from "purify-ts"; import { useMemo } from "react"; import { createSelector } from "reselect"; -import { getTokenPriceInUSD } from "../domain"; import type { PositionBalancesByType } from "../domain/types/positions"; -import type { Prices } from "../domain/types/price"; -import type { usePrices } from "./api/use-prices"; +import type { YieldBalanceDto } from "../providers/yield-api-client-provider/types"; import type { usePositionBalances } from "./use-position-balances"; export const usePositionBalanceByType = ({ positionBalancesData, - prices, - baseToken, }: { positionBalancesData: ReturnType["data"]; - prices: ReturnType>; - baseToken: Maybe; }) => { /** * @summary Position balance by type */ return useMemo( () => - Maybe.fromRecord({ positionBalancesData, baseToken }).map((val) => - getPositionBalanceByTypeWithPrices({ - baseToken: val.baseToken, - prices: prices.data, - pvd: val.positionBalancesData.balances, + positionBalancesData.map((val) => + getPositionBalanceByTypeWithUsd({ + pvd: val.balances, }) ), - [positionBalancesData, prices, baseToken] + [positionBalancesData] ); }; type Args = { - prices: Prices | undefined; pvd: YieldBalanceDto[]; - baseToken: TokenDto; }; -const selectPrices = (val: Args) => val.prices; const selectPvd = (val: Args) => val.pvd; -const selectBaseToken = (val: Args) => val.baseToken; -export const getPositionBalanceByTypeWithPrices = createSelector( - selectPrices, +export const getPositionBalanceByTypeWithUsd = createSelector( selectPvd, - selectBaseToken, - (prices, pvd, baseToken) => + (pvd) => pvd.reduce((acc, cur) => { const amount = new BigNumber(cur.amount); if (amount.isZero() || amount.isNaN()) return acc; - const tokenPriceInUsd = prices - ? getTokenPriceInUSD({ - amount: cur.amount, - prices, - token: cur.token, - pricePerShare: cur.pricePerShare, - baseToken, - }) - : new BigNumber(0); + const tokenPriceInUsd = new BigNumber( + String(cur.amountUsd ?? 0).replace(/,/g, "") + ); const prev = acc.get(cur.type); diff --git a/packages/widget/src/hooks/use-position-balances.ts b/packages/widget/src/hooks/use-position-balances.ts index 85ef2568..613e8641 100644 --- a/packages/widget/src/hooks/use-position-balances.ts +++ b/packages/widget/src/hooks/use-position-balances.ts @@ -1,5 +1,6 @@ import { Maybe } from "purify-ts"; import { useMemo } from "react"; +import type { BalanceDataKey } from "../domain/types/positions"; import { usePositionData } from "./use-position-data"; export const usePositionBalances = ({ @@ -15,10 +16,21 @@ export const usePositionBalances = ({ () => Maybe.fromRecord({ positionData: data, - balanceId: Maybe.fromNullable(balanceId), - }).chainNullable((val) => - val.positionData.balanceData.get(val.balanceId) - ), + balanceId: Maybe.fromNullable(balanceId as BalanceDataKey), + }).chainNullable((val) => { + const balanceData = + val.positionData.balanceData.get(val.balanceId) ?? + val.positionData.balanceData.values().next().value; + + if (!balanceData) { + return undefined; + } + + return { + ...balanceData, + rewardRate: val.positionData.rewardRate, + }; + }), [balanceId, data] ); diff --git a/packages/widget/src/hooks/use-positions-data.ts b/packages/widget/src/hooks/use-positions-data.ts index 1ad3b2b5..657ecdde 100644 --- a/packages/widget/src/hooks/use-positions-data.ts +++ b/packages/widget/src/hooks/use-positions-data.ts @@ -1,11 +1,18 @@ -import type { - YieldBalanceDto, - YieldBalancesWithIntegrationIdDto, -} from "@stakekit/api-hooks"; import { useMemo } from "react"; import { createSelector } from "reselect"; -import type { PositionsData } from "../domain/types/positions"; -import { useYieldBalancesScan } from "./api/use-yield-balances-scan"; +import { + type BalanceDataKey, + getPositionBalanceDataKey, + type PositionsData, +} from "../domain/types/positions"; +import type { + YieldBalanceDto, + YieldBalancesByYieldDto, +} from "../providers/yield-api-client-provider/types"; +import { + normalizeYieldBalancesForPosition, + useYieldBalancesScan, +} from "./api/use-yield-balances-scan"; export const usePositionsData = () => { const { data, ...rest } = useYieldBalancesScan({ @@ -20,41 +27,44 @@ export const usePositionsData = () => { return { data: val, ...rest }; }; -type YieldBalanceDtoID = YieldBalanceDto["groupId"]; - const positionsDataSelector = createSelector( - (balancesData: YieldBalancesWithIntegrationIdDto[]) => balancesData, + (balancesData: YieldBalancesByYieldDto[]) => balancesData, (balancesData) => - balancesData.reduce((acc, val) => { - acc.set(val.integrationId, { - integrationId: val.integrationId, + normalizeYieldBalancesForPosition(balancesData).reduce((acc, val) => { + acc.set(val.yieldId, { + yieldId: val.yieldId, + rewardRate: val.rewardRate, balanceData: [...val.balances] - .sort((a, b) => (a.groupId ?? "").localeCompare(b.groupId ?? "")) + .sort((a, b) => + getPositionBalanceDataKey(a).localeCompare( + getPositionBalanceDataKey(b) + ) + ) .reduce((acc, b) => { - const prev = acc.get(b.groupId); + const key = getPositionBalanceDataKey(b); + const prev = acc.get(key); + const validatorsAddresses = getBalanceValidatorAddresses(b); if (prev) { prev.balances.push(b); } else { - if (b.validatorAddresses || b.validatorAddress || b.providerId) { - acc.set(b.groupId, { + if (key === "default") { + acc.set(key, { balances: [b], - type: "validators", - validatorsAddresses: - b.validatorAddresses ?? - (b.providerId ? [b.providerId] : [b.validatorAddress!]), + type: "default", }); } else { - acc.set(b.groupId, { + acc.set(key, { balances: [b], - type: "default", + type: "validators", + validatorsAddresses, }); } } return acc; }, new Map< - YieldBalanceDtoID, + BalanceDataKey, { balances: YieldBalanceDto[] } & ( | { type: "validators"; validatorsAddresses: string[] } | { type: "default" } @@ -65,3 +75,9 @@ const positionsDataSelector = createSelector( return acc; }, new Map() as PositionsData) ); + +const getBalanceValidatorAddresses = (balance: YieldBalanceDto) => + ( + balance.validators?.map((validator) => validator.address) ?? + (balance.validator?.address ? [balance.validator.address] : []) + ).filter(Boolean); diff --git a/packages/widget/src/hooks/use-provider-details.ts b/packages/widget/src/hooks/use-provider-details.ts index 9975dee5..e0150a09 100644 --- a/packages/widget/src/hooks/use-provider-details.ts +++ b/packages/widget/src/hooks/use-provider-details.ts @@ -8,6 +8,7 @@ import { import type { GetMaybeJust } from "../types/utils"; import { getRewardRateFormatted } from "../utils/formatters"; import { useMultiYields } from "./api/use-multi-yields"; +import { useYieldValidators } from "./api/use-yield-validators"; type Res = Maybe<{ logo: string | undefined; @@ -29,11 +30,13 @@ export const getProviderDetails = ({ validatorAddress, yields, selectedProviderYieldId, + validatorsData, }: { integrationData: Maybe; validatorAddress: Maybe; yields: Maybe; selectedProviderYieldId: Maybe; + validatorsData?: ValidatorDto[]; }): Res => { const def = integrationData.chain((val) => { const rewardRate = val.rewardRate; @@ -70,7 +73,7 @@ export const getProviderDetails = ({ .chain>((addr) => List.find( (v) => v.address === addr || v.providerId === addr, - yieldDto.validators + validatorsData ?? [] ).map((validator) => { const { rewardRate, rewardType } = Maybe.fromRecord({ _: Maybe.fromFalsy(isYieldWithProviderOptions(yieldDto)), @@ -115,15 +118,51 @@ export const useProvidersDetails = ({ integrationData, validatorsAddresses, selectedProviderYieldId, + validatorsData, }: { integrationData: Maybe; validatorsAddresses: Maybe>; selectedProviderYieldId: Maybe; + validatorsData?: Maybe; }) => { const yields = useMultiYields( integrationData.map(getYieldProviderYieldIds).orDefault([]) ); + const shouldFetchValidators = validatorsAddresses + .filter((val): val is string[] => !(val instanceof Map)) + .map((val) => val.length > 0) + .chain((val) => + validatorsData?.isJust() ? Maybe.of(false) : Maybe.of(val) + ) + .orDefault(false); + + const yieldValidators = useYieldValidators({ + enabled: shouldFetchValidators, + yieldId: + integrationData.map((val) => val.id).extractNullable() ?? undefined, + network: + integrationData.map((val) => val.token.network).extractNullable() ?? + undefined, + }); + + const resolvedValidatorsData = useMemo( + () => + validatorsData?.altLazy(() => + validatorsAddresses.chain((val) => + val instanceof Map + ? Maybe.of([...val.values()]) + : Maybe.fromNullable(yieldValidators.data) + ) + ) ?? + validatorsAddresses.chain((val) => + val instanceof Map + ? Maybe.of([...val.values()]) + : Maybe.fromNullable(yieldValidators.data) + ), + [validatorsAddresses, validatorsData, yieldValidators.data] + ); + return useMemo>[]>>( () => validatorsAddresses.chain((val) => @@ -137,6 +176,8 @@ export const useProvidersDetails = ({ validatorAddress: Maybe.of(v), yields: Maybe.fromNullable(yields.data), selectedProviderYieldId, + validatorsData: + resolvedValidatorsData.extractNullable() ?? undefined, }) ) ).chain((val) => @@ -150,6 +191,12 @@ export const useProvidersDetails = ({ }).map((v) => [v]) ) ), - [integrationData, validatorsAddresses, yields.data, selectedProviderYieldId] + [ + integrationData, + validatorsAddresses, + yields.data, + selectedProviderYieldId, + resolvedValidatorsData, + ] ); }; diff --git a/packages/widget/src/hooks/use-rich-errors.ts b/packages/widget/src/hooks/use-rich-errors.ts index fe75657e..71d8076e 100644 --- a/packages/widget/src/hooks/use-rich-errors.ts +++ b/packages/widget/src/hooks/use-rich-errors.ts @@ -10,17 +10,40 @@ interface RichError { const $richError = new BehaviorSubject(null); +const isRichError = (error: unknown): error is RichError => + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string"; + +export const handleRichErrorResponse = ({ + data, + i18n, + url, +}: { + data: unknown; + i18n: i18n; + url?: string; +}) => { + if (!isRichError(data)) { + return; + } + + if (i18n.exists(`errors.${data.message}`) && !url?.includes("gas-estimate")) { + $richError.next(data); + } +}; + export const attachRichErrorsInterceptor = ( apiClient: AxiosInstance, i18n: i18n ) => apiClient.interceptors.response.use(undefined, (error) => { - if ( - i18n.exists(`errors.${error?.response?.data?.message}`) && - !error?.config?.url.includes("gas-estimate") // temp ignore gas estimate errors - ) { - $richError.next(error.response.data); - } + handleRichErrorResponse({ + data: error?.response?.data, + i18n, + url: error?.config?.url, + }); return Promise.reject(error); }); diff --git a/packages/widget/src/hooks/use-staked-or-liquid-balance.ts b/packages/widget/src/hooks/use-staked-or-liquid-balance.ts index 9a19eedd..544a81eb 100644 --- a/packages/widget/src/hooks/use-staked-or-liquid-balance.ts +++ b/packages/widget/src/hooks/use-staked-or-liquid-balance.ts @@ -8,9 +8,7 @@ export const useStakedOrLiquidBalance = ( return useMemo( () => positionBalancesByType.chain((pbbt) => - Maybe.fromNullable(pbbt.get("staked")).altLazy(() => - Maybe.fromNullable(pbbt.get("available")) - ) + Maybe.fromNullable(pbbt.get("active")) ), [positionBalancesByType] ); diff --git a/packages/widget/src/hooks/use-summary.tsx b/packages/widget/src/hooks/use-summary.tsx index 50f30cc1..2b880a3c 100644 --- a/packages/widget/src/hooks/use-summary.tsx +++ b/packages/widget/src/hooks/use-summary.tsx @@ -20,17 +20,19 @@ import { const SummaryContext = createContext< | { - allPositionsQuery: UseQueryResult< - { - allPositions: { - yieldName: string; - usdAmount: number; - providerDetails: ReturnType; - }[]; - allPositionsSum: BigNumber; - }, - StakeKitErrorDto - >; + allPositionsQuery: { + data: + | { + allPositions: { + yieldName: string; + usdAmount: number; + providerDetails: ReturnType; + }[]; + allPositionsSum: BigNumber; + } + | undefined; + isLoading: boolean; + }; rewardsPositionsQuery: UseQueryResult< { rewardsPositions: { @@ -45,7 +47,10 @@ const SummaryContext = createContext< }, StakeKitErrorDto >; - averageApyQuery: UseQueryResult; + averageApyQuery: { + data: BigNumber | undefined; + isLoading: boolean; + }; availableBalanceSumQuery: UseQueryResult; } | undefined @@ -86,84 +91,57 @@ export const SummaryProvider = ({ ), }); - const allPositionsQuery = usePrices( - { - currency: config.currency, - tokenList: useMemo(() => { - if (!multiYieldsMapQuery.data) return []; - - return positionsData.data.flatMap((v) => { - const yieldDto = multiYieldsMapQuery.data.get(v.integrationId); - - if (!yieldDto) return []; - - const baseToken = getBaseToken(yieldDto); - - return [...v.allBalances.map((b) => b.token), baseToken]; - }); - }, [multiYieldsMapQuery.data, positionsData.data]), - }, - { - enabled: !multiYieldsMapQuery.isLoading, - select: useCallback( - (prices: Prices) => { - if (!positionsData.data || !multiYieldsMapQuery.data) { - return { allPositions: [], allPositionsSum: new BigNumber(0) }; - } - - const allPositions = positionsData.data.flatMap((p) => { - const yieldDto = multiYieldsMapQuery.data.get(p.integrationId); - - if (!yieldDto) return []; - - const baseToken = getBaseToken(yieldDto); - - const pricePerShare = "1"; - - const positionTotalAmount = getPositionTotalAmount({ - token: { ...baseToken, pricePerShare }, - balances: p.balancesWithAmount, - }); - - const yields = [...multiYieldsMapQuery.data.values()]; - - const providerDetails = getProviderDetails({ - integrationData: Maybe.of(yieldDto), - validatorAddress: - p.type === "validators" - ? List.head(p.validatorsAddresses) - : Maybe.empty(), - selectedProviderYieldId: Maybe.empty(), - yields: Maybe.of(yields), - }); - - return { - yieldName: yieldDto.metadata.name, - providerDetails, - usdAmount: getTokenPriceInUSD({ - baseToken, - amount: positionTotalAmount, - pricePerShare, - token: baseToken, - prices, - }).toNumber(), - }; - }); - - const allPositionsSum = allPositions.reduce( - (acc, p) => acc.plus(p.usdAmount), - new BigNumber(0) - ); - - return { - allPositions, - allPositionsSum, - }; - }, - [multiYieldsMapQuery.data, positionsData.data] - ), + const allPositionsQuery = useMemo(() => { + if (!multiYieldsMapQuery.data) { + return { + data: undefined as undefined, + isLoading: multiYieldsMapQuery.isLoading, + }; } - ); + + const allPositions = positionsData.data.flatMap((p) => { + const yieldDto = multiYieldsMapQuery.data.get(p.integrationId); + + if (!yieldDto) return []; + + const positionTotalAmount = getPositionTotalAmount( + p.balancesWithAmount, + getBaseToken(yieldDto) + ); + + const yields = [...multiYieldsMapQuery.data.values()]; + + const providerDetails = getProviderDetails({ + integrationData: Maybe.of(yieldDto), + validatorAddress: + p.type === "validators" + ? List.head(p.validatorsAddresses) + : Maybe.empty(), + selectedProviderYieldId: Maybe.empty(), + yields: Maybe.of(yields), + }); + + return { + yieldName: yieldDto.metadata.name, + providerDetails, + usdAmount: positionTotalAmount.amountUsd.toNumber(), + }; + }); + + const allPositionsSum = allPositions.reduce( + (acc, p) => acc.plus(p.usdAmount), + new BigNumber(0) + ); + + return { + data: { allPositions, allPositionsSum }, + isLoading: false as const, + }; + }, [ + multiYieldsMapQuery.data, + multiYieldsMapQuery.isLoading, + positionsData.data, + ]); const rewardsPositionsQuery = usePrices( { @@ -246,81 +224,54 @@ export const SummaryProvider = ({ } ); - const averageApyQuery = usePrices( - { - currency: config.currency, - tokenList: useMemo(() => { - if (!multiYieldsMapQuery.data) return []; - - return positionsData.data.flatMap((v) => { - const yieldDto = multiYieldsMapQuery.data.get(v.integrationId); + const averageApyQuery = useMemo(() => { + if (!multiYieldsMapQuery.data) { + return { + data: undefined as undefined, + isLoading: multiYieldsMapQuery.isLoading, + }; + } - if (!yieldDto) return []; + const { totalWeightedApy, totalValue } = positionsData.data.reduce( + (acc, p) => { + const yieldDto = multiYieldsMapQuery.data.get(p.integrationId); - const baseToken = getBaseToken(yieldDto); + if (!yieldDto) return acc; - return [...v.allBalances.map((b) => b.token), baseToken]; - }); - }, [multiYieldsMapQuery.data, positionsData.data]), - }, - { - enabled: !multiYieldsMapQuery.isLoading, - select: useCallback( - (prices: Prices) => { - if (!positionsData.data || !multiYieldsMapQuery.data) { - return new BigNumber(0); - } + const positionTotalAmount = getPositionTotalAmount( + p.balancesWithAmount, + getBaseToken(yieldDto) + ); - const { totalWeightedApy, totalValue } = positionsData.data.reduce( - (acc, p) => { - const yieldDto = multiYieldsMapQuery.data.get(p.integrationId); - - if (!yieldDto) return acc; - - const baseToken = getBaseToken(yieldDto); - - const pricePerShare = "1"; - - const positionTotalAmount = getPositionTotalAmount({ - token: { ...baseToken, pricePerShare }, - balances: p.balancesWithAmount, - }); - - const usdAmount = getTokenPriceInUSD({ - baseToken, - amount: positionTotalAmount, - pricePerShare, - token: baseToken, - prices, - }); - - if (yieldDto.rewardRate > 0 && usdAmount.gt(0)) { - return { - totalWeightedApy: acc.totalWeightedApy.plus( - usdAmount.times(yieldDto.rewardRate * 100) - ), - totalValue: acc.totalValue.plus(usdAmount), - }; - } - - return acc; - }, - { - totalWeightedApy: new BigNumber(0), - totalValue: new BigNumber(0), - } - ); + const usdAmount = positionTotalAmount.amountUsd; - if (totalValue.gt(0)) { - return totalWeightedApy.div(totalValue); - } + if (yieldDto.rewardRate > 0 && usdAmount.gt(0)) { + return { + totalWeightedApy: acc.totalWeightedApy.plus( + usdAmount.times(yieldDto.rewardRate * 100) + ), + totalValue: acc.totalValue.plus(usdAmount), + }; + } - return new BigNumber(0); - }, - [multiYieldsMapQuery.data, positionsData.data] - ), - } - ); + return acc; + }, + { + totalWeightedApy: new BigNumber(0), + totalValue: new BigNumber(0), + } + ); + + const data = totalValue.gt(0) + ? totalWeightedApy.div(totalValue) + : new BigNumber(0); + + return { data, isLoading: false as const }; + }, [ + multiYieldsMapQuery.data, + multiYieldsMapQuery.isLoading, + positionsData.data, + ]); const tokenBalancesScan = useTokenBalancesScan(); diff --git a/packages/widget/src/hooks/use-yield-meta-info.tsx b/packages/widget/src/hooks/use-yield-meta-info.tsx index b614624b..f714e86d 100644 --- a/packages/widget/src/hooks/use-yield-meta-info.tsx +++ b/packages/widget/src/hooks/use-yield-meta-info.tsx @@ -5,6 +5,7 @@ import { type ReactNode, useMemo } from "react"; import { Trans, useTranslation } from "react-i18next"; import { SKAnchor } from "../components/atoms/anchor"; import { isEthenaUsdeStaking } from "../domain/types/yields"; +import type { YieldTokenDto } from "../providers/yield-api-client-provider/types"; import { capitalizeFirstLowerRest } from "../utils/text"; export const useYieldMetaInfo = ({ @@ -16,7 +17,7 @@ export const useYieldMetaInfo = ({ validators: { [Key in keyof Pick]?: ValidatorDto[Key]; }[]; - tokenDto: Maybe; + tokenDto: Maybe; }) => { const { t } = useTranslation(); diff --git a/packages/widget/src/pages-dashboard/activity/position-balances.tsx b/packages/widget/src/pages-dashboard/activity/position-balances.tsx index 6faab230..8678ed06 100644 --- a/packages/widget/src/pages-dashboard/activity/position-balances.tsx +++ b/packages/widget/src/pages-dashboard/activity/position-balances.tsx @@ -1,9 +1,10 @@ -import type { YieldBalanceDto, YieldDto } from "@stakekit/api-hooks"; +import type { YieldDto } from "@stakekit/api-hooks"; import type BigNumber from "bignumber.js"; import { useTranslation } from "react-i18next"; import { Box } from "../../components/atoms/box"; import { TokenIcon } from "../../components/atoms/token-icon"; import { Text } from "../../components/atoms/typography/text"; +import type { YieldBalanceDto } from "../../providers/yield-api-client-provider/types"; import { defaultFormattedNumber } from "../../utils"; export const PositionBalances = ({ diff --git a/packages/widget/src/pages-dashboard/overview/earn-page/utila-select-validator-section.tsx b/packages/widget/src/pages-dashboard/overview/earn-page/utila-select-validator-section.tsx index 5c2e2981..4e34baae 100644 --- a/packages/widget/src/pages-dashboard/overview/earn-page/utila-select-validator-section.tsx +++ b/packages/widget/src/pages-dashboard/overview/earn-page/utila-select-validator-section.tsx @@ -25,7 +25,7 @@ export const UtilaSelectValidatorSection = () => { ) : ( Maybe.fromRecord({ selectedStake, validatorsData }) - .filter((val) => !!val.selectedStake.validators.length) + .filter((val) => !!val.validatorsData.length) .map((val) => { const selectedValidatorsArr = [...selectedValidators.values()]; diff --git a/packages/widget/src/pages-dashboard/overview/positions/hooks/use-position-list-item.ts b/packages/widget/src/pages-dashboard/overview/positions/hooks/use-position-list-item.ts index 91d6e7ee..f58a28b5 100644 --- a/packages/widget/src/pages-dashboard/overview/positions/hooks/use-position-list-item.ts +++ b/packages/widget/src/pages-dashboard/overview/positions/hooks/use-position-list-item.ts @@ -18,9 +18,8 @@ export const usePositionListItem = ( rewardRateAverage, inactiveValidator, baseToken, - totalAmount, + totalAmountUsd, totalAmountFormatted, - tokenToDisplay, } = useBasePositionListItem(item); const rewardsSummaryQuery = useRewardsSummary(item.integrationId); @@ -37,7 +36,6 @@ export const usePositionListItem = ( currency: config.currency, tokenList: [ ...baseToken.mapOrDefault((v) => [v], []), - ...tokenToDisplay.mapOrDefault((v) => [v], []), ...rewardsSummary.mapOrDefault((v) => [v.token], []), ], }); @@ -52,23 +50,10 @@ export const usePositionListItem = ( const totalAmountPriceFormatted = useMemo( () => - Maybe.fromRecord({ - totalAmount, - baseToken, - prices: Maybe.fromNullable(prices.data), - tokenToDisplay, - }) - .map((val) => - getTokenPriceInUSD({ - baseToken: val.baseToken, - amount: val.totalAmount, - pricePerShare: val.tokenToDisplay.pricePerShare, - token: val.tokenToDisplay, - prices: val.prices, - }) - ) + totalAmountUsd + .filter((v) => v.isGreaterThan(0)) .map(defaultFormattedNumber), - [totalAmount, baseToken, prices, tokenToDisplay] + [totalAmountUsd] ); const rewardsAmountPriceFormatted = useMemo( diff --git a/packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx b/packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx index 70e67d52..7cacbed5 100644 --- a/packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx +++ b/packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx @@ -1,4 +1,3 @@ -import type { ActionTypes } from "@stakekit/api-hooks"; import { Maybe } from "purify-ts"; import { useTranslation } from "react-i18next"; import { Box } from "../../../components/atoms/box"; @@ -8,6 +7,7 @@ import { SelectValidator } from "../../../components/molecules/select-validator" import { AmountBlock } from "../../../pages/position-details/components/amount-block"; import { StaticActionBlock } from "../../../pages/position-details/components/static-action-block"; import { usePositionDetails } from "../../../pages/position-details/hooks/use-position-details"; +import type { YieldPendingActionType } from "../../../providers/yield-api-client-provider/types"; import { container } from "./styles.css"; export const positionDetailsActionsHasContent = ( @@ -34,6 +34,7 @@ export const PositionDetailsActions = () => { const { isLoading, integrationData, + validatorsData, positionBalancesByType, unstakeToken, providersDetails, @@ -102,7 +103,7 @@ export const PositionDetailsActions = () => { } label={t( `position_details.pending_action_button.${ - val.pendingActionDto.type.toLowerCase() as Lowercase + val.pendingActionDto.type.toLowerCase() as Lowercase }` )} onMaxClick={null} @@ -173,7 +174,7 @@ export const PositionDetailsActions = () => { onValidatorsSubmit([val.address]); }} selectedStake={v.integrationData} - validators={v.integrationData.validators} + validators={validatorsData} multiSelect={validatorAddressesHandling.multiSelect} state={validatorAddressesHandling.modalState} > diff --git a/packages/widget/src/pages-dashboard/position-details/components/position-details-info.tsx b/packages/widget/src/pages-dashboard/position-details/components/position-details-info.tsx index 0e666712..005cfef1 100644 --- a/packages/widget/src/pages-dashboard/position-details/components/position-details-info.tsx +++ b/packages/widget/src/pages-dashboard/position-details/components/position-details-info.tsx @@ -7,7 +7,6 @@ import { CollapsibleRoot, CollapsibleTrigger, } from "../../../components/atoms/collapsible"; -import { InfoIcon } from "../../../components/atoms/icons/info"; import { Spinner } from "../../../components/atoms/spinner"; import { Text } from "../../../components/atoms/typography/text"; import { PositionBalances } from "../../../pages/position-details/components/position-balances"; @@ -21,7 +20,6 @@ export const PositionDetailsInfo = () => { integrationData, positionBalancesByType, providersDetails, - positionLabel, liquidTokensToNativeConversion, } = usePositionDetails(); @@ -51,38 +49,6 @@ export const PositionDetailsInfo = () => { px="4" py="4" > - {positionLabel - .map((l) => ( - - - - - - - - { - t( - `position_details.labels.${l.type}.details`, - l.params - ) as string - } - - - - )) - .extractNullable()} - {providersDetails .map((pd) => diff --git a/packages/widget/src/pages/complete/pages/common.page.tsx b/packages/widget/src/pages/complete/pages/common.page.tsx index c2621c1f..ed0d1bf8 100644 --- a/packages/widget/src/pages/complete/pages/common.page.tsx +++ b/packages/widget/src/pages/complete/pages/common.page.tsx @@ -1,9 +1,4 @@ -import type { - ActionTypes, - TokenDto, - YieldDto, - YieldMetadataDto, -} from "@stakekit/api-hooks"; +import type { TokenDto, YieldDto, YieldMetadataDto } from "@stakekit/api-hooks"; import { motion } from "motion/react"; import { Just, Maybe } from "purify-ts"; import { useTranslation } from "react-i18next"; @@ -19,6 +14,10 @@ import { isEthenaUsdeStaking, } from "../../../domain/types/yields"; import { AnimationPage } from "../../../navigation/containers/animation-page"; +import type { + YieldPendingActionType, + YieldTokenDto, +} from "../../../providers/yield-api-client-provider/types"; import { capitalizeFirstLowerRest } from "../../../utils/text"; import { PageContainer } from "../../components/page-container"; import { useComplete } from "../hooks/use-complete.hook"; @@ -28,11 +27,11 @@ import { } from "../state"; type Props = { - token: Maybe; + token: Maybe; metadata: Maybe; network: string; amount: string; - pendingActionType?: ActionTypes; + pendingActionType?: YieldPendingActionType; providersDetails: Maybe< { logo: string | undefined; @@ -133,7 +132,7 @@ export const CompletePageComponent = ({ tokenNetwork: network, pendingAction: t( `complete.pending_action.${ - pendingActionType?.toLowerCase() as Lowercase + pendingActionType?.toLowerCase() as Lowercase }` as const, { context: isEthenaUsdeStaking(integrationId) diff --git a/packages/widget/src/pages/complete/pages/pending-complete.page.tsx b/packages/widget/src/pages/complete/pages/pending-complete.page.tsx index b47d1c11..fa1c17b2 100644 --- a/packages/widget/src/pages/complete/pages/pending-complete.page.tsx +++ b/packages/widget/src/pages/complete/pages/pending-complete.page.tsx @@ -48,10 +48,10 @@ export const PendingCompletePage = () => { const network = token.mapOrDefault((t) => t.symbol, ""); const amount = useMemo( () => - Maybe.fromNullable(pendingRequest.requestDto.args?.amount) + Maybe.fromNullable(pendingRequest.requestDto.arguments?.amount) .map((val) => new BigNumber(val ?? 0)) .mapOrDefault((v) => formatNumber(v), ""), - [pendingRequest.requestDto.args?.amount] + [pendingRequest.requestDto.arguments?.amount] ); const yieldType = useYieldType(integrationData).map((v) => v.type); diff --git a/packages/widget/src/pages/complete/pages/stake-complete.page.tsx b/packages/widget/src/pages/complete/pages/stake-complete.page.tsx index 04c70429..efd8003c 100644 --- a/packages/widget/src/pages/complete/pages/stake-complete.page.tsx +++ b/packages/widget/src/pages/complete/pages/stake-complete.page.tsx @@ -32,15 +32,18 @@ export const StakeCompletePage = () => { const network = selectedToken.mapOrDefault((y) => y.symbol, ""); const amount = useMemo( - () => formatNumber(new BigNumber(enterRequest.requestDto.args.amount)), - [enterRequest.requestDto.args.amount] + () => + formatNumber( + new BigNumber(enterRequest.requestDto.arguments?.amount ?? 0) + ), + [enterRequest.requestDto.arguments?.amount] ); const yieldType = useYieldType(selectedStake).map((v) => v.type); const selectedProviderYieldId = useMemo( - () => Maybe.fromNullable(enterRequest.requestDto.args.providerId), - [enterRequest.requestDto.args.providerId] + () => Maybe.fromNullable(enterRequest.requestDto.arguments?.providerId), + [enterRequest.requestDto.arguments?.providerId] ); const providerDetails = useProvidersDetails({ diff --git a/packages/widget/src/pages/complete/pages/unstake-complete.page.tsx b/packages/widget/src/pages/complete/pages/unstake-complete.page.tsx index b6f5b2cd..79b2a927 100644 --- a/packages/widget/src/pages/complete/pages/unstake-complete.page.tsx +++ b/packages/widget/src/pages/complete/pages/unstake-complete.page.tsx @@ -45,8 +45,8 @@ export const UnstakeCompletePage = () => { const metadata = integrationData.map((d) => d.metadata); const network = token.mapOrDefault((t) => t.symbol, ""); const amount = useMemo( - () => formatNumber(exitRequest.requestDto.args.amount), - [exitRequest.requestDto.args.amount] + () => formatNumber(exitRequest.requestDto.arguments?.amount ?? 0), + [exitRequest.requestDto.arguments?.amount] ); const yieldType = useYieldType(integrationData).map((v) => v.type); diff --git a/packages/widget/src/pages/details/earn-page/components/select-validator-section/index.tsx b/packages/widget/src/pages/details/earn-page/components/select-validator-section/index.tsx index def33fb8..0fde334a 100644 --- a/packages/widget/src/pages/details/earn-page/components/select-validator-section/index.tsx +++ b/packages/widget/src/pages/details/earn-page/components/select-validator-section/index.tsx @@ -27,7 +27,7 @@ export const SelectValidatorSection = () => { ) : ( Maybe.fromRecord({ selectedStake, validatorsData }) - .filter((val) => !!val.selectedStake.validators.length) + .filter((val) => !!val.validatorsData.length) .map((val) => { const selectedValidatorsArr = [...selectedValidators.values()]; diff --git a/packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx b/packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx index 031a622a..21e7b24e 100644 --- a/packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx +++ b/packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx @@ -5,10 +5,12 @@ import { MorphoStarsIcon } from "../../../../../components/atoms/icons/morpho-st import { Image } from "../../../../../components/atoms/image"; import { ImageFallback } from "../../../../../components/atoms/image-fallback"; import { Text } from "../../../../../components/atoms/typography/text"; +import { RewardRateBreakdown } from "../../../../../components/molecules/reward-rate-breakdown"; import { isMorphoProvider, RewardTokenDetails, } from "../../../../../components/molecules/reward-token-details"; +import { getYieldRewardRateDetails } from "../../../../../domain/types/reward-rate"; import { VerticalDivider } from "../../../../../pages-dashboard/common/components/divider"; import { useSettings } from "../../../../../providers/settings"; import { combineRecipeWithVariant } from "../../../../../utils/styles"; @@ -17,10 +19,15 @@ import { selectYieldRewardsText } from "./styles.css"; export const SelectYieldRewardDetails = () => { const { variant } = useSettings(); + const { t } = useTranslation(); - const { rewardToken, estimatedRewards, rewardsTokenSymbol } = + const { rewardToken, estimatedRewards, rewardsTokenSymbol, selectedStake } = useEarnPageContext(); + const rewardRateDetails = selectedStake.chainNullable( + getYieldRewardRateDetails + ); + const earnYearly = estimatedRewards.mapOrDefault( (e) => `${e.yearly} ${rewardsTokenSymbol}`, "" @@ -113,6 +120,17 @@ export const SelectYieldRewardDetails = () => { earnYearly={earnYearly} /> )} + + {rewardRateDetails + .map((rewardRate) => ( + + )) + .extractNullable()} ); diff --git a/packages/widget/src/pages/details/earn-page/components/select-yield-section/staked-via.tsx b/packages/widget/src/pages/details/earn-page/components/select-yield-section/staked-via.tsx index da8af183..ad9c5c51 100644 --- a/packages/widget/src/pages/details/earn-page/components/select-yield-section/staked-via.tsx +++ b/packages/widget/src/pages/details/earn-page/components/select-yield-section/staked-via.tsx @@ -15,7 +15,8 @@ export const StakedVia = () => { (val) => !!( val.metadata.type === "staking" && - !val.validators.length && + !val.args.enter.args?.validatorAddress?.required && + !val.args.enter.args?.validatorAddresses?.required && val.metadata.provider ) ) diff --git a/packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx b/packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx index b08348e3..21860e2a 100644 --- a/packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx +++ b/packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx @@ -16,6 +16,7 @@ import { useDeferredValue, useEffect, useMemo, + useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; @@ -26,6 +27,7 @@ import { stakeTokenSameAsGasToken, tokenString, } from "../../../../domain"; +import { getInitSelectedValidators } from "../../../../domain/types/stake"; import { type ExtendedYieldType, getExtendedYieldType, @@ -39,11 +41,13 @@ import { useStreamMultiYields } from "../../../../hooks/api/use-multi-yields"; import { useTokenBalancesScan } from "../../../../hooks/api/use-token-balances-scan"; import { useTokensPrices } from "../../../../hooks/api/use-tokens-prices"; import { useYieldOpportunity } from "../../../../hooks/api/use-yield-opportunity"; +import { useYieldValidators } from "../../../../hooks/api/use-yield-validators"; import { useNavigateWithScrollToTop } from "../../../../hooks/navigation/use-navigate-with-scroll-to-top"; import { useTrackEvent } from "../../../../hooks/tracking/use-track-event"; import { useAddLedgerAccount } from "../../../../hooks/use-add-ledger-account"; import { useBaseToken } from "../../../../hooks/use-base-token"; import { useEstimatedRewards } from "../../../../hooks/use-estimated-rewards"; +import { useInitParams } from "../../../../hooks/use-init-params"; import { useMaxMinYieldAmount } from "../../../../hooks/use-max-min-yield-amount"; import { usePositionsData } from "../../../../hooks/use-positions-data"; import { useProvidersDetails } from "../../../../hooks/use-provider-details"; @@ -92,6 +96,7 @@ export const EarnPageContextProvider = ({ children }: PropsWithChildren) => { const dispatch = useEarnPageDispatch(); const { t } = useTranslation(); + const initParams = useInitParams(); const baseToken = useBaseToken(selectedStake); @@ -314,19 +319,89 @@ export const EarnPageContextProvider = ({ children }: PropsWithChildren) => { [deferredStakeSearch, multiYields, t] ); + const yieldValidators = useYieldValidators({ + enabled: selectedStake.isJust(), + yieldId: selectedStake.extract()?.id, + network: selectedStake.extract()?.token.network, + }); + + const initialValidatorSelectionYieldIdRef = useRef(null); + + useEffect(() => { + const currentYieldId = selectedStake.map((val) => val.id).extractNullable(); + + if (!currentYieldId) { + initialValidatorSelectionYieldIdRef.current = null; + return; + } + + if (selectedValidators.size > 0) { + initialValidatorSelectionYieldIdRef.current = currentYieldId; + return; + } + + if (initialValidatorSelectionYieldIdRef.current === currentYieldId) { + return; + } + + if (!yieldValidators.isFetched && !yieldValidators.isError) { + return; + } + + initialValidatorSelectionYieldIdRef.current = currentYieldId; + + const shouldSelectDefaultValidator = selectedStake + .map( + (stake) => + !!( + stake.metadata.isIntegrationAggregator || + stake.args.enter.args?.validatorAddress?.required || + stake.args.enter.args?.validatorAddresses?.required + ) + ) + .orDefault(false); + + if (!shouldSelectDefaultValidator) { + return; + } + + const nextValidator = List.head([ + ...getInitSelectedValidators({ + initQueryParams: Maybe.fromNullable(initParams.data), + validators: yieldValidators.data ?? [], + }).values(), + ]).extractNullable(); + + if (nextValidator) { + dispatch({ type: "validator/select", data: nextValidator }); + } + }, [ + dispatch, + initParams.data, + selectedStake, + selectedValidators, + yieldValidators.data, + yieldValidators.isError, + yieldValidators.isFetched, + ]); + const validatorsData = useMemo( () => - selectedStake.chain((ss) => - Maybe.fromNullable(deferredValidatorSearch) - .map((val) => val.toLowerCase()) - .map((searchInput) => - ss.validators.filter( + selectedStake.chain(() => + Maybe.fromNullable(yieldValidators.data) + .map((validators) => { + const searchInput = deferredValidatorSearch.toLowerCase(); + + if (!searchInput) { + return validators; + } + + return validators.filter( (validator) => validator.name?.toLowerCase().includes(searchInput) || validator.address.toLowerCase().includes(searchInput) - ) - ) - .alt(Maybe.of(ss.validators)) + ); + }) .map((validators) => { if (variant === "utila" || variant === "porto") { return [...validators].sort( @@ -337,7 +412,7 @@ export const EarnPageContextProvider = ({ children }: PropsWithChildren) => { return validators; }) ), - [deferredValidatorSearch, selectedStake, variant] + [deferredValidatorSearch, selectedStake, variant, yieldValidators.data] ); const onYieldSearch: SelectModalProps["onSearch"] = (val) => @@ -395,6 +470,7 @@ export const EarnPageContextProvider = ({ children }: PropsWithChildren) => { enterStakeStore.send({ type: "initFlow", data: { + addresses: val.stakeEnterRequestDto.addresses, requestDto: val.stakeEnterRequestDto.dto, selectedToken: val.selectedToken, gasFeeToken: val.stakeEnterRequestDto.gasFeeToken, @@ -583,7 +659,9 @@ export const EarnPageContextProvider = ({ children }: PropsWithChildren) => { defaultTokensIsLoading || tokenBalancesScanLoading || initYieldRes.isLoading || - yieldOpportunityLoading; + yieldOpportunityLoading || + yieldValidators.isLoading || + yieldValidators.isFetching; const footerIsLoading = defaultTokensIsLoading || diff --git a/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx b/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx index 1b12b3a8..72c34d2d 100644 --- a/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx +++ b/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx @@ -14,7 +14,6 @@ import { import { equalTokens } from "../../../../domain"; import { isNetworkWithEnterMinBasedOnPosition } from "../../../../domain/types/stake"; import { useYieldOpportunity } from "../../../../hooks/api/use-yield-opportunity"; -import { useInitParams } from "../../../../hooks/use-init-params"; import { useMaxMinYieldAmount } from "../../../../hooks/use-max-min-yield-amount"; import { usePositionsData } from "../../../../hooks/use-positions-data"; import { useSavedRef } from "../../../../hooks/use-saved-ref"; @@ -51,12 +50,12 @@ const getInitialState = (): State => ({ selectedStakeId: Maybe.empty(), selectedValidators: new Map(), stakeAmount: new BigNumber(0), + useMaxAmount: false, tronResource: Maybe.empty(), selectedProviderYieldId: Maybe.empty(), }); export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { - const initParams = useInitParams(); const { network, isConnected } = useSKWallet(); const getInitYield = useGetInitYield(); @@ -75,7 +74,6 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { getInitYield({ selectedToken: action.data }) .map | null>((val) => onYieldSelectState({ - initParams: Maybe.fromNullable(initParams.data), yieldDto: val, positionsData: positionsData.data, }) @@ -96,7 +94,6 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { ) .map(() => onYieldSelectState({ - initParams: Maybe.fromNullable(initParams.data), yieldDto: action.data, positionsData: positionsData.data, }) @@ -154,6 +151,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { return { ...state, stakeAmount: action.data, + useMaxAmount: false, }; } @@ -161,6 +159,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { return { ...state, stakeAmount: action.data, + useMaxAmount: true, }; } @@ -184,6 +183,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { selectedStakeId, selectedValidators, stakeAmount: _stakeAmount, + useMaxAmount, tronResource, selectedProviderYieldId, } = state; @@ -344,6 +344,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { selectedStake, selectedValidators, stakeAmount, + useMaxAmount, actions, tronResource, stakeAmountGreaterThanAvailableAmount, @@ -362,6 +363,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { selectedToken, selectedValidators, stakeAmount, + useMaxAmount, actions, tronResource, stakeAmountGreaterThanAvailableAmount, diff --git a/packages/widget/src/pages/details/earn-page/state/types.ts b/packages/widget/src/pages/details/earn-page/state/types.ts index b81bc4bd..54ebd1e0 100644 --- a/packages/widget/src/pages/details/earn-page/state/types.ts +++ b/packages/widget/src/pages/details/earn-page/state/types.ts @@ -20,6 +20,7 @@ export type State = { >; selectedValidators: Map; stakeAmount: BigNumber; + useMaxAmount: boolean; tronResource: Maybe; selectedProviderYieldId: Maybe; }; diff --git a/packages/widget/src/pages/details/earn-page/state/use-init-token.ts b/packages/widget/src/pages/details/earn-page/state/use-init-token.ts index d89e7f41..9e46d597 100644 --- a/packages/widget/src/pages/details/earn-page/state/use-init-token.ts +++ b/packages/widget/src/pages/details/earn-page/state/use-init-token.ts @@ -11,6 +11,7 @@ import { useValidatorsConfig } from "../../../../hooks/use-validators-config"; import { useSKQueryClient } from "../../../../providers/query-client"; import { useSettings } from "../../../../providers/settings"; import { useSKWallet } from "../../../../providers/sk-wallet"; +import { useYieldApiFetchClient } from "../../../../providers/yield-api-client-provider"; import { useGetTokenBalancesMap } from "./use-get-token-balances-map"; /** @@ -28,6 +29,7 @@ export const useInitToken = () => { isConnecting, } = useSKWallet(); const queryClient = useSKQueryClient(); + const yieldApiFetchClient = useYieldApiFetchClient(); const { data: positionsData } = usePositionsData(); const { @@ -60,8 +62,8 @@ export const useInitToken = () => { getInitParams({ isLedgerLive, queryClient, + yieldApiFetchClient, externalProviders, - validatorsConfig, }).chain((initParams) => EitherAsync.liftEither( getInitialToken({ @@ -83,6 +85,7 @@ export const useInitToken = () => { isConnected, isLedgerLive, queryClient, + yieldApiFetchClient, network, yieldIds: tokenBalance.availableYields, initParams: initParams, diff --git a/packages/widget/src/pages/details/earn-page/state/use-init-yield.ts b/packages/widget/src/pages/details/earn-page/state/use-init-yield.ts index de775868..1e2e3c06 100644 --- a/packages/widget/src/pages/details/earn-page/state/use-init-yield.ts +++ b/packages/widget/src/pages/details/earn-page/state/use-init-yield.ts @@ -11,6 +11,7 @@ import { useValidatorsConfig } from "../../../../hooks/use-validators-config"; import { useSKQueryClient } from "../../../../providers/query-client"; import { useSettings } from "../../../../providers/settings"; import { useSKWallet } from "../../../../providers/sk-wallet"; +import { useYieldApiFetchClient } from "../../../../providers/yield-api-client-provider"; import { useGetTokenBalancesMap } from "./use-get-token-balances-map"; export const useInitYield = ({ @@ -28,6 +29,7 @@ export const useInitYield = ({ isConnecting, } = useSKWallet(); const queryClient = useSKQueryClient(); + const yieldApiFetchClient = useYieldApiFetchClient(); const { externalProviders, tokensForEnabledYieldsOnly, @@ -71,13 +73,14 @@ export const useInitYield = ({ getInitParams({ isLedgerLive, queryClient, + yieldApiFetchClient, externalProviders, - validatorsConfig, }).chain((initParams) => getFirstEligibleYield({ isConnected, isLedgerLive, queryClient, + yieldApiFetchClient, network, yieldIds: val.availableYields, initParams: initParams, diff --git a/packages/widget/src/pages/details/earn-page/state/use-pending-action-deep-link.ts b/packages/widget/src/pages/details/earn-page/state/use-pending-action-deep-link.ts index b32a301c..348e8e45 100644 --- a/packages/widget/src/pages/details/earn-page/state/use-pending-action-deep-link.ts +++ b/packages/widget/src/pages/details/earn-page/state/use-pending-action-deep-link.ts @@ -1,9 +1,6 @@ -import { - type AddressWithTokenDtoAdditionalAddresses, - type PendingActionDto, - type YieldBalanceDto, - type YieldDto, - yieldGetSingleYieldBalances, +import type { + AddressWithTokenDtoAdditionalAddresses, + YieldDto, } from "@stakekit/api-hooks"; import type { QueryClient } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; @@ -12,13 +9,17 @@ import { PAMultiValidatorsRequired, PASingleValidatorRequired, } from "../../../../domain"; -import type { ValidatorsConfig } from "../../../../domain/types/yields"; +import { getPositionBalanceDataKey } from "../../../../domain/types/positions"; import { getYieldOpportunity } from "../../../../hooks/api/use-yield-opportunity/get-yield-opportunity"; import { getInitParams } from "../../../../hooks/use-init-params"; -import { useValidatorsConfig } from "../../../../hooks/use-validators-config"; import { useSKQueryClient } from "../../../../providers/query-client"; import { useSettings } from "../../../../providers/settings"; import { useSKWallet } from "../../../../providers/sk-wallet"; +import { useYieldApiFetchClient } from "../../../../providers/yield-api-client-provider"; +import type { + YieldBalanceDto, + YieldPendingActionDto, +} from "../../../../providers/yield-api-client-provider/types"; import type { GetEitherRight, Override } from "../../../../types/utils"; import { preparePendingActionRequestDto } from "../../../position-details/hooks/utils"; @@ -27,11 +28,10 @@ export const usePendingActionDeepLink = () => { useSKWallet(); const queryClient = useSKQueryClient(); + const yieldApiFetchClient = useYieldApiFetchClient(); const { externalProviders } = useSettings(); - const validatorsConfig = useValidatorsConfig(); - return useQuery({ staleTime: Number.POSITIVE_INFINITY, gcTime: Number.POSITIVE_INFINITY, @@ -49,8 +49,8 @@ export const usePendingActionDeepLink = () => { additionalAddresses, address: addr, queryClient, + yieldApiFetchClient, externalProviders, - validatorsConfig, }) ) ).unsafeCoerce(), @@ -62,21 +62,21 @@ const fn = ({ additionalAddresses, address, queryClient, + yieldApiFetchClient, externalProviders, - validatorsConfig, }: { isLedgerLive: boolean; address: string; additionalAddresses: AddressWithTokenDtoAdditionalAddresses | null; queryClient: QueryClient; + yieldApiFetchClient: ReturnType; externalProviders: ReturnType["externalProviders"]; - validatorsConfig: ValidatorsConfig; }) => getInitParams({ isLedgerLive, queryClient, + yieldApiFetchClient, externalProviders, - validatorsConfig, }).chain((val) => { const initQueryParams = Maybe.of(val) .filter( @@ -94,19 +94,29 @@ const fn = ({ return EitherAsync.liftEither(initQueryParams) .chain((initQueryParams) => EitherAsync(() => - yieldGetSingleYieldBalances(initQueryParams.yieldId, { - addresses: { + yieldApiFetchClient.POST("/v1/yields/{yieldId}/balances", { + params: { + path: { + yieldId: initQueryParams.yieldId, + }, + }, + body: { address, - additionalAddresses: additionalAddresses ?? undefined, }, }) ) - .mapLeft(() => new Error("could not get yield balances")) + .chain((response) => + EitherAsync.liftEither( + Maybe.fromNullable(response.data).toEither( + new Error("could not get yield balances") + ) + ) + ) .map((val) => ({ yieldId: initQueryParams.yieldId, pendingaction: initQueryParams.pendingaction, validatorAddress: initQueryParams.validator, - singleYieldBalances: val, + singleYieldBalances: val.balances, address: address, additionalAddresses: additionalAddresses ?? undefined, })) @@ -117,7 +127,10 @@ const fn = ({ for (const balance of balances) { if ( data.validatorAddress && - balance.validatorAddress !== data.validatorAddress + balance.validator?.address !== data.validatorAddress && + !balance.validators?.some( + (validator) => validator.address === data.validatorAddress + ) ) { continue; } @@ -130,7 +143,7 @@ const fn = ({ return Right({ pendingAction, balance, - balanceId: balance.groupId ?? "default", + balanceId: getPositionBalanceDataKey(balance), }); } } @@ -143,7 +156,7 @@ const fn = ({ isLedgerLive, yieldId: data.yieldId, queryClient, - validatorsConfig, + yieldApiFetchClient, }).map((yieldOp) => ({ ...val, yieldOp })) ) .chain< @@ -151,7 +164,7 @@ const fn = ({ | { type: "positionDetails"; yieldOp: YieldDto; - pendingAction: PendingActionDto; + pendingAction: YieldPendingActionDto; balance: YieldBalanceDto; balanceId: string; } diff --git a/packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts b/packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts index 235ddd5f..c2c98736 100644 --- a/packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts +++ b/packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts @@ -1,17 +1,16 @@ -import type { - ActionRequestDto, - ValidatorDto, - YieldDto, -} from "@stakekit/api-hooks"; +import type { AddressesDto, ValidatorDto, YieldDto } from "@stakekit/api-hooks"; import { Just, List, Maybe } from "purify-ts"; import { useMemo } from "react"; import { useSKWallet } from "../../../../providers/sk-wallet"; +import { withAdditionalAddresses } from "../../../../providers/yield-api-client-provider/request-helpers"; +import type { YieldCreateActionDto } from "../../../../providers/yield-api-client-provider/types"; import { useEarnPageState } from "./earn-page-state-context"; export const useStakeEnterRequestDto = () => { const { selectedStake, stakeAmount, + useMaxAmount, selectedValidators, tronResource, selectedToken, @@ -26,16 +25,23 @@ export const useStakeEnterRequestDto = () => { selectedStake, selectedToken, }).map<{ + addresses: AddressesDto; gasFeeToken: YieldDto["token"]; - dto: ActionRequestDto; + dto: YieldCreateActionDto; selectedValidators: Map; selectedStake: YieldDto; }>(({ address, selectedStake, selectedToken }) => { const validatorsOrProvider = Just(selectedStake) .chain< - | Pick - | Pick - | Pick + | Pick< + NonNullable, + "validatorAddresses" + > + | Pick< + NonNullable, + "validatorAddress" | "subnetId" + > + | Pick, "providerId"> >((val) => { const validators = [...selectedValidators.values()]; @@ -64,20 +70,25 @@ export const useStakeEnterRequestDto = () => { selectedValidators, selectedStake: selectedStake, gasFeeToken: selectedStake.metadata.gasFeeToken, + addresses: { + address, + additionalAddresses: additionalAddresses ?? undefined, + }, dto: { - addresses: { - address: address, - additionalAddresses: additionalAddresses ?? undefined, - }, - integrationId: selectedStake.id, - args: { - inputToken: selectedToken, - ledgerWalletAPICompatible: isLedgerLive ?? undefined, - tronResource: tronResource.extract(), - amount: stakeAmount.toString(10), - providerId: selectedProviderYieldId.extract(), - ...validatorsOrProvider, - }, + address, + yieldId: selectedStake.id, + arguments: withAdditionalAddresses({ + additionalAddresses, + argumentsDto: { + inputToken: selectedToken.address, + ledgerWalletApiCompatible: isLedgerLive ?? undefined, + tronResource: tronResource.extract(), + amount: stakeAmount.toString(10), + useMaxAmount: useMaxAmount || undefined, + providerId: selectedProviderYieldId.extract(), + ...validatorsOrProvider, + }, + }), }, }; }), @@ -89,6 +100,7 @@ export const useStakeEnterRequestDto = () => { selectedToken, selectedValidators, stakeAmount, + useMaxAmount, tronResource, selectedProviderYieldId, ] diff --git a/packages/widget/src/pages/details/earn-page/state/utils.ts b/packages/widget/src/pages/details/earn-page/state/utils.ts index 146eff39..7edf87c5 100644 --- a/packages/widget/src/pages/details/earn-page/state/utils.ts +++ b/packages/widget/src/pages/details/earn-page/state/utils.ts @@ -1,21 +1,15 @@ import type { YieldDto } from "@stakekit/api-hooks"; import { List, Maybe } from "purify-ts"; -import type { InitParams } from "../../../../domain/types/init-params"; import type { PositionsData } from "../../../../domain/types/positions"; -import { - getInitSelectedValidators, - getMinStakeAmount, -} from "../../../../domain/types/stake"; +import { getMinStakeAmount } from "../../../../domain/types/stake"; import type { State } from "./types"; export const onYieldSelectState = ({ yieldDto, positionsData, - initParams, }: { yieldDto: YieldDto; positionsData: PositionsData; - initParams: Maybe; }): Pick< State, | "selectedStakeId" @@ -26,10 +20,7 @@ export const onYieldSelectState = ({ > => ({ selectedStakeId: Maybe.of(yieldDto.id), stakeAmount: getMinStakeAmount(yieldDto, positionsData), - selectedValidators: getInitSelectedValidators({ - initQueryParams: initParams, - yieldDto: yieldDto, - }), + selectedValidators: new Map(), tronResource: Maybe.fromFalsy( yieldDto.args.enter.args?.tronResource?.required ).map(() => "ENERGY"), diff --git a/packages/widget/src/pages/details/positions-page/hooks/use-position-list-item.ts b/packages/widget/src/pages/details/positions-page/hooks/use-position-list-item.ts index 28f99e26..3ea1c9cb 100644 --- a/packages/widget/src/pages/details/positions-page/hooks/use-position-list-item.ts +++ b/packages/widget/src/pages/details/positions-page/hooks/use-position-list-item.ts @@ -63,15 +63,17 @@ export const usePositionListItem = ( [integrationData] ); - const totalAmount = useMemo( + const amounts = useMemo( () => - tokenToDisplay.map((val) => - getPositionTotalAmount({ - token: val, - balances: item.balancesWithAmount, - }) - ), - [item.balancesWithAmount, tokenToDisplay] + baseToken.map((b) => getPositionTotalAmount(item.balancesWithAmount, b)), + [item.balancesWithAmount, baseToken] + ); + + const totalAmount = useMemo(() => amounts.map((v) => v.amount), [amounts]); + + const totalAmountUsd = useMemo( + () => amounts.map((v) => v.amountUsd), + [amounts] ); const totalAmountFormatted = useMemo( @@ -85,6 +87,7 @@ export const usePositionListItem = ( rewardRateAverage, inactiveValidator, totalAmount, + totalAmountUsd, totalAmountFormatted, baseToken, tokenToDisplay, diff --git a/packages/widget/src/pages/details/positions-page/hooks/use-positions.ts b/packages/widget/src/pages/details/positions-page/hooks/use-positions.ts index bdba10cb..c1dec633 100644 --- a/packages/widget/src/pages/details/positions-page/hooks/use-positions.ts +++ b/packages/widget/src/pages/details/positions-page/hooks/use-positions.ts @@ -1,17 +1,16 @@ -import type { - YieldBalanceDto, - YieldBalanceLabelDto, - YieldBalancesWithIntegrationIdDto, -} from "@stakekit/api-hooks"; +import type { YieldBalanceLabelDto } from "@stakekit/api-hooks"; import BigNumber from "bignumber.js"; -import { compare, Just, List, type Maybe } from "purify-ts"; +import { compare, Just, List, Maybe } from "purify-ts"; import { useMemo } from "react"; import { createSelector } from "reselect"; -import type { YieldFindValidatorsParams } from "../../../../common/private-api"; import { usePositionsData } from "../../../../hooks/use-positions-data"; import { useSettings } from "../../../../providers/settings"; import type { SettingsContextType } from "../../../../providers/settings/types"; import { useSKWallet } from "../../../../providers/sk-wallet"; +import type { + YieldBalanceDto, + YieldBalanceType, +} from "../../../../providers/yield-api-client-provider/types"; import { defaultFormattedNumber } from "../../../../utils"; export const usePositions = () => { @@ -72,26 +71,20 @@ const positionsTableDataSelector = createSelector( .ifJust((v) => acc.push({ ...value, - integrationId: val.integrationId, + integrationId: val.yieldId, balancesWithAmount: v, balanceId: id, allBalances: value.balances, - yieldLabelDto: List.find( - (b) => !!b.label, - value.balances - ).chainNullable((v) => v.label), + yieldLabelDto: Maybe.empty() as Maybe, token: List.head( List.sort( (a, b) => compare(priorityOrder[a.type], priorityOrder[b.type]), value.balances ) - ).map((v) => ({ - ...v.token, - pricePerShare: v.pricePerShare, - })), + ).map((v) => v.token), actionRequired: v.some( - (b) => b.type === "locked" || b.type === "unstaked" + (b) => b.type === "locked" || b.type === "claimable" ), pointsRewardTokenBalances: v .filter((v) => !!v.token.isPoints) @@ -99,17 +92,11 @@ const positionsTableDataSelector = createSelector( ...v, amount: defaultFormattedNumber(v.amount), })), - hasPendingClaimRewards: List.find( - (b) => b.type === "rewards", - v - ) - .chain((b) => - List.find( - (a) => a.type === "CLAIM_REWARDS", - b.pendingActions - ) + hasPendingClaimRewards: v.some((balance) => + balance.pendingActions.some( + (action) => action.type === "CLAIM_REWARDS" ) - .isJust(), + ), }) ); }); @@ -117,14 +104,14 @@ const positionsTableDataSelector = createSelector( return acc; }, [] as ({ - integrationId: YieldBalancesWithIntegrationIdDto["integrationId"]; + integrationId: string; balancesWithAmount: YieldBalanceDto[]; allBalances: YieldBalanceDto[]; - balanceId: YieldBalanceDto["groupId"]; + balanceId: string; actionRequired: boolean; pointsRewardTokenBalances: YieldBalanceDto[]; hasPendingClaimRewards: boolean; - token: Maybe; + token: Maybe; yieldLabelDto: Maybe; } & ( | { type: "validators"; validatorsAddresses: string[] } @@ -144,19 +131,11 @@ const positionsTableDataSelector = createSelector( .unsafeCoerce() ); -const priorityOrder: { [key in YieldBalanceDto["type"]]: number } = { - available: 1, - staked: 2, - unstaking: 3, - unstaked: 4, - preparing: 5, +const priorityOrder: Record = { + active: 1, + entering: 2, + exiting: 3, + withdrawable: 4, + claimable: 5, locked: 6, - unlocking: 7, - rewards: 8, -}; - -export const getYieldFindValidatorsQueryKey = ( - params?: YieldFindValidatorsParams -) => { - return ["/v1/yields/validators", ...(params ? [params] : [])] as const; }; diff --git a/packages/widget/src/pages/position-details/components/amount-block.tsx b/packages/widget/src/pages/position-details/components/amount-block.tsx index 4244598b..d6f201ad 100644 --- a/packages/widget/src/pages/position-details/components/amount-block.tsx +++ b/packages/widget/src/pages/position-details/components/amount-block.tsx @@ -14,6 +14,7 @@ import { import { Text } from "../../../components/atoms/typography/text"; import * as AmountToggle from "../../../components/molecules/amount-toggle"; import { useYieldMetaInfo } from "../../../hooks/use-yield-meta-info"; +import type { YieldTokenDto } from "../../../providers/yield-api-client-provider/types"; import { defaultFormattedNumber, formatNumber } from "../../../utils"; import { priceTxt } from "../styles.css"; @@ -27,11 +28,11 @@ type AmountBlockProps = { onMaxClick: (() => void) | null; label: string; formattedAmount: string; - balance: { amount: BigNumber; token: TokenDto } | null; + balance: { amount: BigNumber; token: TokenDto | YieldTokenDto } | null; } & ( | { variant: "unstake"; - unstakeToken: TokenDto; + unstakeToken: TokenDto | YieldTokenDto; yieldDto: YieldDto; validators: { [Key in keyof Pick< @@ -221,7 +222,7 @@ const UnstakeInfo = ({ validators: { [Key in keyof Pick]?: ValidatorDto[Key]; }[]; - unstakeToken: TokenDto; + unstakeToken: TokenDto | YieldTokenDto; }) => { const { withdrawnTime, withdrawnNotAvailable, positionLocked } = useYieldMetaInfo({ diff --git a/packages/widget/src/pages/position-details/components/position-balances.tsx b/packages/widget/src/pages/position-details/components/position-balances.tsx index 1322140a..deb8d991 100644 --- a/packages/widget/src/pages/position-details/components/position-balances.tsx +++ b/packages/widget/src/pages/position-details/components/position-balances.tsx @@ -1,4 +1,4 @@ -import type { YieldBalanceDto, YieldDto } from "@stakekit/api-hooks"; +import type { YieldDto } from "@stakekit/api-hooks"; import BigNumber from "bignumber.js"; import { isPast } from "date-fns"; import { useMemo } from "react"; @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import { Box } from "../../../components/atoms/box"; import { TokenIcon } from "../../../components/atoms/token-icon"; import { Text } from "../../../components/atoms/typography/text"; +import type { YieldBalanceDto } from "../../../providers/yield-api-client-provider/types"; import { defaultFormattedNumber } from "../../../utils"; import { formatDurationUntilDate } from "../../../utils/date"; @@ -21,9 +22,7 @@ export const PositionBalances = ({ const durationUntilDate = useMemo(() => { if ( !yieldBalance.date || - (yieldBalance.type !== "unstaking" && - yieldBalance.type !== "unlocking" && - yieldBalance.type !== "preparing") + (yieldBalance.type !== "entering" && yieldBalance.type !== "exiting") ) { return null; } diff --git a/packages/widget/src/pages/position-details/components/provider-details.tsx b/packages/widget/src/pages/position-details/components/provider-details.tsx index 8cbdaa93..e46cf9c4 100644 --- a/packages/widget/src/pages/position-details/components/provider-details.tsx +++ b/packages/widget/src/pages/position-details/components/provider-details.tsx @@ -24,7 +24,10 @@ export const ProviderDetails = ({ integrationData, logo, ...providerDetails -}: { +}: Omit< + GetMaybeJust>[0], + "rewardType" +> & { isFirst: boolean; stakeType: string; integrationData: YieldDto; @@ -32,8 +35,8 @@ export const ProviderDetails = ({ name: string; rewardRateFormatted: string; rewardRate: number | undefined; - rewardType: RewardTypes; -} & GetMaybeJust>[0]) => { + rewardType?: RewardTypes; +}) => { const { t } = useTranslation(); const nameOrAddress = providerDetails.name ?? providerDetails ?? ""; diff --git a/packages/widget/src/pages/position-details/components/static-action-block.tsx b/packages/widget/src/pages/position-details/components/static-action-block.tsx index 4dd6f267..61ba274d 100644 --- a/packages/widget/src/pages/position-details/components/static-action-block.tsx +++ b/packages/widget/src/pages/position-details/components/static-action-block.tsx @@ -1,20 +1,20 @@ -import type { - ActionTypes, - PendingActionDto, - YieldBalanceDto, - YieldDto, -} from "@stakekit/api-hooks"; +import type { YieldDto } from "@stakekit/api-hooks"; import BigNumber from "bignumber.js"; import { Trans, useTranslation } from "react-i18next"; import { Box } from "../../../components/atoms/box"; import { Button } from "../../../components/atoms/button"; import { Text } from "../../../components/atoms/typography/text"; import { isEthenaUsdeStaking } from "../../../domain/types/yields"; +import type { + YieldBalanceDto, + YieldPendingActionDto, + YieldPendingActionType, +} from "../../../providers/yield-api-client-provider/types"; import { formatNumber } from "../../../utils"; import type { usePositionDetails } from "../hooks/use-position-details"; type StaticActionBlockProps = { - pendingActionDto: PendingActionDto; + pendingActionDto: YieldPendingActionDto; yieldBalance: YieldBalanceDto & { tokenPriceInUsd: BigNumber; }; @@ -53,7 +53,7 @@ export const StaticActionBlock = ({ symbol: yieldBalance.token.symbol, pendingAction: t( `position_details.pending_action.${ - pendingActionDto.type.toLowerCase() as Lowercase + pendingActionDto.type.toLowerCase() as Lowercase }`, { context: isEthenaUsdeStaking(yieldId) @@ -91,7 +91,7 @@ export const StaticActionBlock = ({ {t( `position_details.pending_action_button.${ - pendingActionDto.type.toLowerCase() as Lowercase + pendingActionDto.type.toLowerCase() as Lowercase }` )} diff --git a/packages/widget/src/pages/position-details/hooks/use-pending-actions.ts b/packages/widget/src/pages/position-details/hooks/use-pending-actions.ts index 820a3802..4c0cee9e 100644 --- a/packages/widget/src/pages/position-details/hooks/use-pending-actions.ts +++ b/packages/widget/src/pages/position-details/hooks/use-pending-actions.ts @@ -1,9 +1,4 @@ -import type { - PendingActionDto, - ValidatorDto, - YieldBalanceDto, - YieldDto, -} from "@stakekit/api-hooks"; +import type { ValidatorDto, YieldDto } from "@stakekit/api-hooks"; import BigNumber from "bignumber.js"; import { Left, List, Maybe, Right } from "purify-ts"; import { useEffect, useMemo, useRef } from "react"; @@ -13,12 +8,17 @@ import { PAMultiValidatorsRequired, PASingleValidatorRequired, } from "../../../domain"; +import { isPendingActionAmountRequired } from "../../../domain/types/pending-action"; import { usePendingActionSelectValidatorMatch } from "../../../hooks/navigation/use-pending-action-select-validator-match"; import { useTrackEvent } from "../../../hooks/tracking/use-track-event"; import { useBaseToken } from "../../../hooks/use-base-token"; import { useSavedRef } from "../../../hooks/use-saved-ref"; import { usePendingActionStore } from "../../../providers/pending-action-store"; import { useSKWallet } from "../../../providers/sk-wallet"; +import type { + YieldBalanceDto, + YieldPendingActionDto, +} from "../../../providers/yield-api-client-provider/types"; import { defaultFormattedNumber } from "../../../utils"; import { useUnstakeOrPendingActionDispatch, @@ -54,8 +54,8 @@ export const usePendingActions = () => { val.flatMap((balance) => balance.pendingActions.map((pa) => { const amount = Maybe.fromPredicate( - (v) => !!v, - pa.args?.args?.amount?.required + (v) => v, + isPendingActionAmountRequired(pa) ).chain(() => Maybe.fromNullable( pendingActionsState.get( @@ -79,7 +79,7 @@ export const usePendingActions = () => { amount: val.amount, token: val.reducedStakedOrLiquidBalance.token, prices: val.prices, - pricePerShare: balance.pricePerShare, + pricePerShare: null, baseToken: val.baseToken, }) ) @@ -150,7 +150,7 @@ export const usePendingActions = () => { yieldBalance, pendingActionDto, }: { - pendingActionDto: PendingActionDto; + pendingActionDto: YieldPendingActionDto; yieldBalance: YieldBalanceDto; }) => { trackEvent("pendingActionClicked", { @@ -235,7 +235,7 @@ export const usePendingActions = () => { selectedValidators, }: { integrationData: YieldDto; - pendingActionDto: PendingActionDto; + pendingActionDto: YieldPendingActionDto; yieldBalance: YieldBalanceDto; selectedValidators: ValidatorDto["address"][]; }) => { diff --git a/packages/widget/src/pages/position-details/hooks/use-position-details.ts b/packages/widget/src/pages/position-details/hooks/use-position-details.ts index 57c40ca6..64cb3052 100644 --- a/packages/widget/src/pages/position-details/hooks/use-position-details.ts +++ b/packages/widget/src/pages/position-details/hooks/use-position-details.ts @@ -4,8 +4,9 @@ import BigNumber from "bignumber.js"; import { Maybe } from "purify-ts"; import { useMemo } from "react"; import { useNavigate } from "react-router"; -import { equalTokens, getTokenPriceInUSD } from "../../../domain"; +import { equalTokens } from "../../../domain"; import { isForceMaxAmount } from "../../../domain/types/stake"; +import { useYieldValidators } from "../../../hooks/api/use-yield-validators"; import { useTrackEvent } from "../../../hooks/tracking/use-track-event"; import { useBaseToken } from "../../../hooks/use-base-token"; import { useProvidersDetails } from "../../../hooks/use-provider-details"; @@ -72,6 +73,7 @@ export const usePositionDetails = () => { exitStore.send({ type: "initFlow", data: { + addresses: val.stakeExitRequestDto.addresses, gasFeeToken: val.stakeExitRequestDto.gasFeeToken, integrationData: val.integrationData, requestDto: val.stakeExitRequestDto.dto, @@ -90,28 +92,38 @@ export const usePositionDetails = () => { const _unstakeAmountError = onClickHandler.isError || unstakeAmountError; - const positionLabel = useMemo( - () => - positionBalances.data.chainNullable( - (b) => b.balances.find((b) => b.label)?.label - ), - [positionBalances.data] - ); - const dispatch = useUnstakeOrPendingActionDispatch(); const trackEvent = useTrackEvent(); const baseToken = useBaseToken(integrationData); + const yieldValidators = useYieldValidators({ + enabled: integrationData.isJust(), + yieldId: + integrationData.map((val) => val.id).extractNullable() ?? undefined, + network: + integrationData.map((val) => val.token.network).extractNullable() ?? + undefined, + }); + const providersDetails = useProvidersDetails({ integrationData, validatorsAddresses: positionBalances.data.map((b) => { return b.type === "validators" ? b.validatorsAddresses : []; }), selectedProviderYieldId: Maybe.empty(), + validatorsData: Maybe.fromNullable(yieldValidators.data), }); + const personalizedRewardRate = useMemo( + () => + positionBalances.data + .map((balanceData) => balanceData.rewardRate) + .extractNullable(), + [positionBalances.data] + ); + const canUnstake = integrationData.filter((d) => !!d.args.exit).isJust(); const onUnstakeAmountChange = (value: BigNumber) => @@ -119,27 +131,10 @@ export const usePositionDetails = () => { const unstakeFormattedAmount = useMemo( () => - Maybe.fromRecord({ - prices: Maybe.fromNullable(positionBalancePrices.data), - reducedStakedOrLiquidBalance, - baseToken, - }) - .map((val) => - getTokenPriceInUSD({ - amount: unstakeAmount, - token: val.reducedStakedOrLiquidBalance.token, - prices: val.prices, - pricePerShare: val.reducedStakedOrLiquidBalance.pricePerShare, - baseToken: val.baseToken, - }) - ) + reducedStakedOrLiquidBalance + .map((val) => val.amountUsd) .mapOrDefault((v) => `$${defaultFormattedNumber(v)}`, ""), - [ - positionBalancePrices.data, - reducedStakedOrLiquidBalance, - unstakeAmount, - baseToken, - ] + [reducedStakedOrLiquidBalance] ); const onMaxClick = () => { @@ -175,14 +170,14 @@ export const usePositionDetails = () => { .filter( (yb) => !yb.token.isPoints && - yb.pricePerShare && + !!yb.validator?.pricePerShare && !equalTokens(yb.token, v.baseToken) ) .forEach((yb) => { acc.set( yb.token.symbol, `1 ${yb.token.symbol} = ${defaultFormattedNumber( - new BigNumber(yb.pricePerShare) + new BigNumber(yb.validator?.pricePerShare ?? 0) )} ${v.baseToken.symbol}` ); }); @@ -198,10 +193,12 @@ export const usePositionDetails = () => { const isLoading = positionBalances.isLoading || positionBalancePrices.isLoading || - yieldOpportunity.isLoading; + yieldOpportunity.isLoading || + yieldValidators.isLoading; return { integrationData, + validatorsData: yieldValidators.data ?? [], reducedStakedOrLiquidBalance, positionBalancesByType, canUnstake, @@ -215,13 +212,13 @@ export const usePositionDetails = () => { isLoading, onPendingActionClick, providersDetails, + personalizedRewardRate, pendingActions, liquidTokensToNativeConversion, validatorAddressesHandling, onValidatorsSubmit, onPendingActionAmountChange, unstakeToken, - positionLabel, unstakeAmountError: _unstakeAmountError, unstakeMaxAmount, unstakeMinAmount, diff --git a/packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts b/packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts index 83b93997..a3615b46 100644 --- a/packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts +++ b/packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts @@ -1,13 +1,19 @@ -import type { ActionRequestDto, YieldDto } from "@stakekit/api-hooks"; +import type { AddressesDto, YieldDto } from "@stakekit/api-hooks"; import { Just, List, Maybe } from "purify-ts"; import { useMemo } from "react"; import { useSKWallet } from "../../../providers/sk-wallet"; +import { withAdditionalAddresses } from "../../../providers/yield-api-client-provider/request-helpers"; +import type { YieldCreateActionDto } from "../../../providers/yield-api-client-provider/types"; import { useUnstakeOrPendingActionState } from "../state"; export const useStakeExitRequestDto = () => { const { address, additionalAddresses } = useSKWallet(); - const { unstakeAmount, integrationData, stakedOrLiquidBalances } = - useUnstakeOrPendingActionState(); + const { + unstakeAmount, + unstakeUseMaxAmount, + integrationData, + stakedOrLiquidBalances, + } = useUnstakeOrPendingActionState(); return useMemo( () => @@ -16,52 +22,58 @@ export const useStakeExitRequestDto = () => { integrationData, stakedOrLiquidBalances, }).map<{ + addresses: AddressesDto; gasFeeToken: YieldDto["token"]; - dto: ActionRequestDto; + dto: YieldCreateActionDto; }>((val) => { const validatorsOrProvider = Just(null) .chain< - | Pick - | Pick - | Pick + | Pick< + NonNullable, + "validatorAddresses" + > + | Pick< + NonNullable, + "validatorAddress" | "subnetId" + > + | Pick, "providerId"> >(() => { if (val.integrationData.metadata.isIntegrationAggregator) { return List.find( - (b) => !!b.providerId, + (b) => !!b.validator?.providerId, val.stakedOrLiquidBalances ).map((b) => ({ - providerId: b.providerId, - validatorAddress: b.validatorAddress, + providerId: b.validator?.providerId, + validatorAddress: b.validator?.address, })); } if ( val.integrationData.args.exit?.args?.validatorAddresses?.required ) { return List.find( - (b) => !!b.validatorAddresses, + (b) => !!b.validators?.length, val.stakedOrLiquidBalances - ).map((b) => ({ validatorAddresses: b.validatorAddresses })); + ).map((b) => ({ + validatorAddresses: + b.validators?.map((validator) => validator.address) ?? [], + })); } if ( val.integrationData.args.exit?.args?.validatorAddress?.required ) { return List.find( - (b) => !!b.validatorAddress, + (b) => !!b.validator?.address, val.stakedOrLiquidBalances ).map((b) => { const subnetId = Maybe.fromNullable( val.integrationData.args.exit?.args?.subnetId?.required ) - .chainNullable(() => - val.integrationData.validators.find( - (v) => v.address === b.validatorAddress - ) - ) + .chainNullable(() => b.validator) .map((validator) => validator.subnetId) .extract(); return { - validatorAddress: b.validatorAddress, + validatorAddress: b.validator?.address, subnetId, }; }); @@ -73,16 +85,21 @@ export const useStakeExitRequestDto = () => { return { gasFeeToken: val.integrationData.metadata.gasFeeToken, + addresses: { + address: val.address, + additionalAddresses: additionalAddresses ?? undefined, + }, dto: { - addresses: { - address: val.address, - additionalAddresses: additionalAddresses ?? undefined, - }, - integrationId: val.integrationData.id, - args: { - amount: unstakeAmount.toString(10), - ...validatorsOrProvider, - }, + address: val.address, + yieldId: val.integrationData.id, + arguments: withAdditionalAddresses({ + additionalAddresses, + argumentsDto: { + amount: unstakeAmount.toString(10), + useMaxAmount: unstakeUseMaxAmount || undefined, + ...validatorsOrProvider, + }, + }), }, }; }), @@ -92,6 +109,7 @@ export const useStakeExitRequestDto = () => { stakedOrLiquidBalances, integrationData, unstakeAmount, + unstakeUseMaxAmount, ] ); }; diff --git a/packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts b/packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts index 82b76cb8..ee6a412e 100644 --- a/packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts +++ b/packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts @@ -1,8 +1,5 @@ import type { TransactionVerificationMessageDto } from "@stakekit/api-hooks"; -import { - actionExit, - transactionGetTransactionVerificationMessageForNetwork, -} from "@stakekit/api-hooks"; +import { transactionGetTransactionVerificationMessageForNetwork } from "@stakekit/api-hooks"; import { useMachine } from "@xstate/react"; import type { SnapshotFromStore } from "@xstate/store"; import { useSelector } from "@xstate/store/react"; @@ -15,6 +12,8 @@ import { useTrackEvent } from "../../../hooks/tracking/use-track-event"; import { useSavedRef } from "../../../hooks/use-saved-ref"; import { useExitStakeStore } from "../../../providers/exit-stake-store"; import { useSKWallet } from "../../../providers/sk-wallet"; +import { useYieldApiFetchClient } from "../../../providers/yield-api-client-provider"; +import { createExitAction } from "../../../providers/yield-api-client-provider/actions"; import type { GetMaybeJust } from "../../../types/utils"; export const useUnstakeMachine = ({ onDone }: { onDone: () => void }) => { @@ -26,20 +25,31 @@ export const useUnstakeMachine = ({ onDone }: { onDone: () => void }) => { (state) => state.context.data ).unsafeCoerce(); + const yieldApiFetchClient = useYieldApiFetchClient(); const { network, address, additionalAddresses, signMessage } = useSKWallet(); const machineParams = useSavedRef({ onDone, trackEvent, exitStore, - actionExit, + yieldApiFetchClient, signMessage, transactionGetTransactionVerificationMessageForNetwork, getData: () => Maybe.fromRecord({ network: Maybe.fromNullable(network), address: Maybe.fromNullable(address), - }).map((val) => ({ ...val, ...exitRequest, additionalAddresses })), + }).map((val) => ({ + ...val, + ...exitRequest, + addresses: { + ...exitRequest.addresses, + additionalAddresses: + exitRequest.addresses.additionalAddresses ?? + additionalAddresses ?? + undefined, + }, + })), }); return useMachine(useState(() => getMachine(machineParams))[0]); @@ -52,6 +62,7 @@ const getMachine = ( exitStore: ReturnType; signMessage: ReturnType["signMessage"]; trackEvent: ReturnType; + yieldApiFetchClient: ReturnType; getData: () => Maybe< GetMaybeJust< SnapshotFromStore< @@ -60,7 +71,6 @@ const getMachine = ( > & { network: NonNullable; address: NonNullable; - additionalAddresses: SKWallet["additionalAddresses"]; } >; }> @@ -126,7 +136,7 @@ const getMachine = ( Just: (val) => { ref.current.trackEvent("unstakeClicked", { yieldId: val.integrationData.id, - amount: val.requestDto.args.amount, + amount: val.requestDto.arguments?.amount, }); if ( @@ -172,9 +182,9 @@ const getMachine = ( val.network, { addresses: { - address: val.address, + address: val.addresses.address, additionalAddresses: - val.additionalAddresses ?? undefined, + val.addresses.additionalAddresses ?? undefined, }, } ) @@ -270,40 +280,55 @@ const getMachine = ( }) => EitherAsync.liftEither( data - .map((val) => val.requestDto) - .map((requestDto) => + .map((val) => Maybe.fromRecord({ + data: Maybe.of(val), + requestDto: Maybe.of(val.requestDto), transactionVerificationMessageDto, signedMessage, }) - .map( + .map( (val) => ({ - ...requestDto, - args: { - ...requestDto.args, - signatureVerification: { - message: - val.transactionVerificationMessageDto.message, - signed: val.signedMessage, - }, + ...val.data, + requestDto: { + ...val.requestDto, + address: val.data.addresses.address, + arguments: { + ...(val.requestDto.arguments ?? {}), + // The backend still accepts this legacy verification bag + // even though the checked-in schema does not expose it yet. + signatureVerification: { + message: + val.transactionVerificationMessageDto + .message, + signed: val.signedMessage, + }, + } as typeof val.requestDto.arguments, }, - }) satisfies typeof requestDto + }) as typeof val.data ) - .orDefault(requestDto) + .orDefault(val) ) .toEither(new Error("Missing params")) ) .chain((val) => - EitherAsync(() => actionExit(val)) + EitherAsync(() => + createExitAction({ + addresses: val.addresses, + fetchClient: ref.current.yieldApiFetchClient, + requestDto: val.requestDto, + yieldDto: val.integrationData, + }) + ) .mapLeft(() => new Error("Stake exit error")) .chain((actionDto) => EitherAsync.liftEither(getValidStakeSessionTx(actionDto)) ) - .ifRight((val) => + .ifRight((result) => ref.current.exitStore.send({ type: "setActionDto", - data: val, + data: result, }) ) ) diff --git a/packages/widget/src/pages/position-details/hooks/use-validator-addresses-handling.ts b/packages/widget/src/pages/position-details/hooks/use-validator-addresses-handling.ts index e24391c2..f75e4a2c 100644 --- a/packages/widget/src/pages/position-details/hooks/use-validator-addresses-handling.ts +++ b/packages/widget/src/pages/position-details/hooks/use-validator-addresses-handling.ts @@ -1,10 +1,11 @@ -import type { - PendingActionDto, - ValidatorDto, - YieldBalanceDto, -} from "@stakekit/api-hooks"; +import type { ValidatorDto } from "@stakekit/api-hooks"; import { useCallback, useMemo, useReducer } from "react"; import type { SelectModalProps } from "../../../components/atoms/select-modal"; +import { isPendingActionValidatorAddressesRequired } from "../../../domain/types/pending-action"; +import type { + YieldBalanceDto, + YieldPendingActionDto, +} from "../../../providers/yield-api-client-provider/types"; import type { Action } from "../../../types/utils"; type State = { @@ -14,7 +15,7 @@ type State = { | { showValidatorsModal: true; yieldBalance: YieldBalanceDto; - pendingActionDto: PendingActionDto; + pendingActionDto: YieldPendingActionDto; } | { showValidatorsModal: false; @@ -25,7 +26,7 @@ type State = { type ValidatorOpenAction = Action< "validator/open", - { yieldBalance: YieldBalanceDto; pendingActionDto: PendingActionDto } + { yieldBalance: YieldBalanceDto; pendingActionDto: YieldPendingActionDto } >; type ValidatorCloseAction = Action<"validator/close">; type ValidatorMultiSelectAction = Action< @@ -82,15 +83,18 @@ const reducer = (state: State, action: Actions): State => { } case "validator/open": { - const newSelectedValidators: State["selectedValidators"] = new Set( - action.data.yieldBalance.validatorAddresses - ); + const newSelectedValidators: State["selectedValidators"] = new Set([ + ...(action.data.yieldBalance.validators?.map((v) => v.address) ?? []), + ...(action.data.yieldBalance.validator?.address + ? [action.data.yieldBalance.validator.address] + : []), + ]); return { ...state, - multiSelect: - !!action.data.pendingActionDto.args?.args?.validatorAddresses - ?.required, + multiSelect: isPendingActionValidatorAddressesRequired( + action.data.pendingActionDto + ), selectedValidators: newSelectedValidators, pendingActionDto: action.data.pendingActionDto, yieldBalance: action.data.yieldBalance, @@ -122,7 +126,7 @@ export const useValidatorAddressesHandling = () => { const openModal = useCallback( (args: { yieldBalance: YieldBalanceDto; - pendingActionDto: PendingActionDto; + pendingActionDto: YieldPendingActionDto; }) => dispatch({ type: "validator/open", data: args }), [] ); diff --git a/packages/widget/src/pages/position-details/hooks/utils.ts b/packages/widget/src/pages/position-details/hooks/utils.ts index a8261931..08609efc 100644 --- a/packages/widget/src/pages/position-details/hooks/utils.ts +++ b/packages/widget/src/pages/position-details/hooks/utils.ts @@ -1,16 +1,33 @@ import type { - PendingActionDto, - PendingActionRequestDto, + PendingActionDto as LegacyPendingActionDto, + YieldBalanceDto as LegacyYieldBalanceDto, ValidatorDto, - YieldBalanceDto, YieldDto, } from "@stakekit/api-hooks"; import type { Either } from "purify-ts"; import { List, Maybe } from "purify-ts"; +import { + type AnyPendingActionDto, + isPendingActionAmountRequired, + isPendingActionValidatorAddressesRequired, + isPendingActionValidatorAddressRequired, +} from "../../../domain/types/pending-action"; import type { SKWallet } from "../../../domain/types/wallet"; +import { withAdditionalAddresses } from "../../../providers/yield-api-client-provider/request-helpers"; +import type { + YieldBalanceDto, + YieldCreateManageActionDto, + YieldTokenDto, +} from "../../../providers/yield-api-client-provider/types"; import type { State } from "../state/types"; import { getBalanceTokenActionType } from "../state/utils"; +type AnyYieldBalanceDto = { + amount: string; + token: YieldTokenDto; + type: LegacyYieldBalanceDto["type"] | YieldBalanceDto["type"]; +}; + export const preparePendingActionRequestDto = ({ pendingActionsState, additionalAddresses, @@ -23,14 +40,14 @@ export const preparePendingActionRequestDto = ({ pendingActionsState: State["pendingActions"]; address: SKWallet["address"]; additionalAddresses: SKWallet["additionalAddresses"]; - pendingActionDto: PendingActionDto; - yieldBalance: YieldBalanceDto; + pendingActionDto: AnyPendingActionDto; + yieldBalance: AnyYieldBalanceDto; integration: YieldDto; selectedValidators: ValidatorDto["address"][]; }): Either< Error, { - requestDto: PendingActionRequestDto; + requestDto: YieldCreateManageActionDto; integrationData: YieldDto; gasFeeToken: YieldDto["token"]; address: NonNullable; @@ -42,17 +59,18 @@ export const preparePendingActionRequestDto = ({ Maybe.fromNullable(address) .toEither(new Error("missing address")) .map((val) => { - const args: PendingActionRequestDto["args"] = { + const args: NonNullable = { amount: Maybe.fromPredicate( Boolean, - pendingActionDto.args?.args?.amount?.required + isPendingActionAmountRequired(pendingActionDto) ) .chainNullable(() => pendingActionsState.get( getBalanceTokenActionType({ - balanceType: yieldBalance.type, + balanceType: yieldBalance.type as YieldBalanceDto["type"], token: yieldBalance.token, - actionType: pendingActionDto.type, + actionType: + pendingActionDto.type as LegacyPendingActionDto["type"], }) ) ) @@ -62,19 +80,23 @@ export const preparePendingActionRequestDto = ({ }; if (selectedValidators.length) { - if (pendingActionDto.args?.args?.validatorAddresses?.required) { + if (isPendingActionValidatorAddressesRequired(pendingActionDto)) { args.validatorAddresses = selectedValidators; - } else if (pendingActionDto.args?.args?.validatorAddress?.required) { + } else if (isPendingActionValidatorAddressRequired(pendingActionDto)) { args.validatorAddress = List.head(selectedValidators).orDefault(""); } } return { requestDto: { - args, - integrationId: integration.id, + action: pendingActionDto.type as LegacyPendingActionDto["type"], + address: val, + arguments: withAdditionalAddresses({ + additionalAddresses, + argumentsDto: args, + }), passthrough: pendingActionDto.passthrough, - type: pendingActionDto.type, + yieldId: integration.id, }, address: val, additionalAddresses: additionalAddresses ?? undefined, diff --git a/packages/widget/src/pages/position-details/position-details.page.tsx b/packages/widget/src/pages/position-details/position-details.page.tsx index c45311b8..4b732070 100644 --- a/packages/widget/src/pages/position-details/position-details.page.tsx +++ b/packages/widget/src/pages/position-details/position-details.page.tsx @@ -1,16 +1,18 @@ -import type { ActionTypes } from "@stakekit/api-hooks"; import { Just, Maybe } from "purify-ts"; import { useTranslation } from "react-i18next"; import { Box } from "../../components/atoms/box"; import { Button } from "../../components/atoms/button"; -import { InfoIcon } from "../../components/atoms/icons/info"; import { Spinner } from "../../components/atoms/spinner"; import { TokenIcon } from "../../components/atoms/token-icon"; import { Heading } from "../../components/atoms/typography/heading"; import { Text } from "../../components/atoms/typography/text"; +import { RewardRateBreakdown } from "../../components/molecules/reward-rate-breakdown"; import { SelectValidator } from "../../components/molecules/select-validator"; +import { getRewardTypeFromRateType } from "../../domain/types/reward-rate"; import { useTrackPage } from "../../hooks/tracking/use-track-page"; import { AnimationPage } from "../../navigation/containers/animation-page"; +import type { YieldPendingActionType } from "../../providers/yield-api-client-provider/types"; +import { getRewardRateFormatted } from "../../utils/formatters"; import { PageContainer } from "../components/page-container"; import { AmountBlock } from "./components/amount-block"; import { PositionBalances } from "./components/position-balances"; @@ -24,6 +26,7 @@ const PositionDetails = () => { const { onPendingActionAmountChange, integrationData, + validatorsData, isLoading, reducedStakedOrLiquidBalance, positionBalancesByType, @@ -43,10 +46,10 @@ const PositionDetails = () => { unstakeToken, canUnstake, unstakeAmountError, - positionLabel, unstakeMaxAmount, unstakeMinAmount, unstakeIsGreaterOrLessIntegrationLimitError, + personalizedRewardRate, } = usePositionDetails(); useTrackPage("positionDetails", { @@ -108,41 +111,38 @@ const PositionDetails = () => { )) .extractNullable()} - {positionLabel - .map((l) => ( + {personalizedRewardRate ? ( + - - - - + + {t("position_details.personalized_apy")} + - - { - t( - `position_details.labels.${l.type}.details`, - l.params - ) as string - } - - + + {getRewardRateFormatted({ + rewardRate: personalizedRewardRate.total, + rewardType: getRewardTypeFromRateType( + personalizedRewardRate.rateType + ), + })} + - )) - .extractNullable()} + + + + ) : null} {providersDetails @@ -152,6 +152,12 @@ const PositionDetails = () => { {...p} key={p.address ?? idx} isFirst={idx === 0} + rewardRate={ + personalizedRewardRate ? undefined : p.rewardRate + } + rewardType={ + personalizedRewardRate ? undefined : p.rewardType + } stakeType={t( `position_details.stake_type.${integrationData.metadata.type}` )} @@ -229,7 +235,7 @@ const PositionDetails = () => { } label={t( `position_details.pending_action_button.${ - val.pendingActionDto.type.toLowerCase() as Lowercase + val.pendingActionDto.type.toLowerCase() as Lowercase }` )} onMaxClick={null} @@ -300,7 +306,7 @@ const PositionDetails = () => { onValidatorsSubmit([val.address]); }} selectedStake={integrationData} - validators={integrationData.validators} + validators={validatorsData} multiSelect={validatorAddressesHandling.multiSelect} state={validatorAddressesHandling.modalState} > diff --git a/packages/widget/src/pages/position-details/state/index.tsx b/packages/widget/src/pages/position-details/state/index.tsx index 45e59edc..bbd7d34c 100644 --- a/packages/widget/src/pages/position-details/state/index.tsx +++ b/packages/widget/src/pages/position-details/state/index.tsx @@ -1,10 +1,3 @@ -import type { - ActionTypes, - PendingActionDto, - PriceRequestDto, - TokenDto, - YieldBalanceDto, -} from "@stakekit/api-hooks"; import BigNumber from "bignumber.js"; import { List, Maybe } from "purify-ts"; import type { Dispatch, PropsWithChildren } from "react"; @@ -17,7 +10,7 @@ import { useRef, } from "react"; import { config } from "../../../config"; -import { isForceMaxAmount } from "../../../domain/types/stake"; +import { getPendingActionAmountConfig } from "../../../domain/types/pending-action"; import { isERC4626 } from "../../../domain/types/yields"; import { usePrices } from "../../../hooks/api/use-prices"; import { useYieldOpportunity } from "../../../hooks/api/use-yield-opportunity"; @@ -27,6 +20,12 @@ import { useMaxMinYieldAmount } from "../../../hooks/use-max-min-yield-amount"; import { usePositionBalanceByType } from "../../../hooks/use-position-balance-by-type"; import { usePositionBalances } from "../../../hooks/use-position-balances"; import { useStakedOrLiquidBalance } from "../../../hooks/use-staked-or-liquid-balance"; +import type { + YieldBalanceDto, + YieldPendingActionDto, + YieldPendingActionType, + YieldTokenDto, +} from "../../../providers/yield-api-client-provider/types"; import type { Actions, BalanceTokenActionType, @@ -89,7 +88,7 @@ export const UnstakeOrPendingActionProvider = ({ positionBalances: positionBalances.data, baseToken, }) - .map((val) => ({ + .map((val) => ({ currency: config.currency, tokenList: [ val.baseToken, @@ -105,9 +104,7 @@ export const UnstakeOrPendingActionProvider = ({ * @summary Position balance by type */ const positionBalancesByType = usePositionBalanceByType({ - baseToken, positionBalancesData: positionBalances.data, - prices: positionBalancePrices, }); const stakedOrLiquidBalances = useStakedOrLiquidBalance( @@ -120,15 +117,17 @@ export const UnstakeOrPendingActionProvider = ({ b.reduce( (acc, next) => { acc.amount = acc.amount.plus(new BigNumber(next.amount)); + acc.amountUsd = acc.amountUsd.plus( + new BigNumber(next.amountUsd ?? 0) + ); acc.token = next.token; - acc.pricePerShare = next.pricePerShare; return acc; }, { + amountUsd: new BigNumber(0), amount: new BigNumber(0), token: b[0].token, - pricePerShare: b[0].pricePerShare, } ) ), @@ -139,10 +138,7 @@ export const UnstakeOrPendingActionProvider = ({ () => stakedOrLiquidBalances .chain((balances) => List.head(balances)) - .map((v) => ({ - ...v.token, - pricePerShare: v.pricePerShare, - })), + .map((v) => v.token), [stakedOrLiquidBalances] ); @@ -155,7 +151,7 @@ export const UnstakeOrPendingActionProvider = ({ yieldOpportunity: integrationData, type: "exit", availableAmount: reducedStakedOrLiquidBalance.map((v) => v.amount), - pricePerShare: unstakeToken.map((v) => v.pricePerShare).extractNullable(), + pricePerShare: null, }); const canChangeUnstakeAmount = integrationData.map( @@ -167,7 +163,7 @@ export const UnstakeOrPendingActionProvider = ({ () => new Map< BalanceTokenActionType, - { pendingAction: PendingActionDto; balance: YieldBalanceDto } + { pendingAction: YieldPendingActionDto; balance: YieldBalanceDto } >( positionBalancesByType .map((pbbt) => @@ -201,8 +197,8 @@ export const UnstakeOrPendingActionProvider = ({ }: { state: State["pendingActions"]; balanceType: YieldBalanceDto["type"]; - token: TokenDto; - actionType: ActionTypes; + token: YieldTokenDto; + actionType: YieldPendingActionType; amount: BigNumber; }) => { const key = getBalanceTokenActionType({ actionType, balanceType, token }); @@ -213,20 +209,13 @@ export const UnstakeOrPendingActionProvider = ({ const newMap = new Map(state); newMap.set(key, amount); + const amountConfig = getPendingActionAmountConfig(val.pendingAction); const max = new BigNumber( - val.pendingAction.args?.args?.amount?.maximum ?? - Number.POSITIVE_INFINITY - ); - const min = new BigNumber( - val.pendingAction.args?.args?.amount?.minimum ?? 0 + amountConfig?.maximum ?? Number.POSITIVE_INFINITY ); + const min = new BigNumber(amountConfig?.minimum ?? 0); - if ( - Maybe.fromNullable(val.pendingAction.args?.args?.amount).mapOrDefault( - isForceMaxAmount, - false - ) - ) { + if (amountConfig?.forceMax) { newMap.set(key, new BigNumber(val.balance.amount)); } else if (amount.isLessThan(min)) { newMap.set(key, min); @@ -244,6 +233,7 @@ export const UnstakeOrPendingActionProvider = ({ return { ...state, unstakeAmount: action.data, + unstakeUseMaxAmount: false, }; } @@ -251,6 +241,7 @@ export const UnstakeOrPendingActionProvider = ({ return { ...state, unstakeAmount: maxEnterOrExitAmount, + unstakeUseMaxAmount: true, }; } @@ -271,10 +262,15 @@ export const UnstakeOrPendingActionProvider = ({ const [state, dispatch] = useReducer(reducer, { unstakeAmount: minEnterOrExitAmount, + unstakeUseMaxAmount: false, pendingActions: new Map(), }); - const { pendingActions, unstakeAmount: _ustankeAmount } = state; + const { + pendingActions, + unstakeAmount: _ustankeAmount, + unstakeUseMaxAmount, + } = state; const unstakeAmount = useMemo( () => @@ -346,6 +342,7 @@ export const UnstakeOrPendingActionProvider = ({ unstakeAmountError, unstakeToken, unstakeAmount, + unstakeUseMaxAmount, pendingActions, positionBalancePrices, reducedStakedOrLiquidBalance, @@ -364,6 +361,7 @@ export const UnstakeOrPendingActionProvider = ({ unstakeAmountError, unstakeToken, unstakeAmount, + unstakeUseMaxAmount, pendingActions, positionBalancePrices, reducedStakedOrLiquidBalance, diff --git a/packages/widget/src/pages/position-details/state/types.ts b/packages/widget/src/pages/position-details/state/types.ts index 60c4957c..91da291d 100644 --- a/packages/widget/src/pages/position-details/state/types.ts +++ b/packages/widget/src/pages/position-details/state/types.ts @@ -1,9 +1,4 @@ -import type { - ActionTypes, - TokenDto, - YieldBalanceDto, - YieldDto, -} from "@stakekit/api-hooks"; +import type { TokenDto, YieldDto } from "@stakekit/api-hooks"; import type BigNumber from "bignumber.js"; import type { Maybe } from "purify-ts"; import type { PositionBalancesByType } from "../../../domain/types/positions"; @@ -13,20 +8,25 @@ import type { usePrices } from "../../../hooks/api/use-prices"; import type { useYieldOpportunity } from "../../../hooks/api/use-yield-opportunity"; import type { usePositionBalances } from "../../../hooks/use-position-balances"; import type { useStakedOrLiquidBalance } from "../../../hooks/use-staked-or-liquid-balance"; +import type { + YieldBalanceType, + YieldPendingActionType, + YieldTokenDto, +} from "../../../providers/yield-api-client-provider/types"; import type { Action } from "../../../types/utils"; type UnstakeAmountChange = Action<"unstake/amount/change", BigNumber>; type UnstakeAmountMax = Action<"unstake/amount/max">; export type BalanceTokenActionType = - `${YieldBalanceDto["type"]}-${TokenString}-${ActionTypes}`; + `${YieldBalanceType}-${TokenString}-${YieldPendingActionType}`; export type PendingActionAmountChange = Action< "pendingAction/amount/change", { - balanceType: YieldBalanceDto["type"]; - token: TokenDto; - actionType: ActionTypes; + balanceType: YieldBalanceType; + token: TokenDto | YieldTokenDto; + actionType: YieldPendingActionType; amount: BigNumber; } >; @@ -38,11 +38,12 @@ export type Actions = export type State = { unstakeAmount: BigNumber; + unstakeUseMaxAmount: boolean; pendingActions: Map; }; export type ExtraData = { - pendingActionType: Maybe; + pendingActionType: Maybe; integrationData: Maybe; positionBalances: ReturnType; yieldOpportunity: ReturnType; @@ -50,12 +51,12 @@ export type ExtraData = { stakedOrLiquidBalances: ReturnType; reducedStakedOrLiquidBalance: Maybe<{ amount: BigNumber; - token: TokenDto; - pricePerShare: string; + amountUsd: BigNumber; + token: TokenDto | YieldTokenDto; }>; positionBalancePrices: ReturnType>; unstakeAmountValid: boolean; - unstakeToken: Maybe; + unstakeToken: Maybe; unstakeAmountError: boolean; canChangeUnstakeAmount: Maybe; unstakeIsGreaterOrLessIntegrationLimitError: boolean; diff --git a/packages/widget/src/pages/position-details/state/utils.ts b/packages/widget/src/pages/position-details/state/utils.ts index d07f150b..bec195ba 100644 --- a/packages/widget/src/pages/position-details/state/utils.ts +++ b/packages/widget/src/pages/position-details/state/utils.ts @@ -1,9 +1,10 @@ -import type { - ActionTypes, - TokenDto, - YieldBalanceDto, -} from "@stakekit/api-hooks"; +import type { TokenDto } from "@stakekit/api-hooks"; import { tokenString } from "../../../domain"; +import type { + YieldBalanceType, + YieldPendingActionType, + YieldTokenDto, +} from "../../../providers/yield-api-client-provider/types"; import type { BalanceTokenActionType } from "./types"; export const getBalanceTokenActionType = ({ @@ -11,8 +12,8 @@ export const getBalanceTokenActionType = ({ balanceType, token, }: { - balanceType: YieldBalanceDto["type"]; - token: TokenDto; - actionType: ActionTypes; + balanceType: YieldBalanceType; + token: TokenDto | YieldTokenDto; + actionType: YieldPendingActionType; }): BalanceTokenActionType => `${balanceType}-${tokenString(token)}-${actionType}`; diff --git a/packages/widget/src/pages/review/hooks/use-fees.ts b/packages/widget/src/pages/review/hooks/use-fees.ts index 73d1a94b..f77e3988 100644 --- a/packages/widget/src/pages/review/hooks/use-fees.ts +++ b/packages/widget/src/pages/review/hooks/use-fees.ts @@ -4,6 +4,7 @@ import { Just, type Maybe } from "purify-ts"; import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { Prices } from "../../../domain/types/price"; +import type { YieldTokenDto } from "../../../providers/yield-api-client-provider/types"; import { bpsToAmount, bpsToPercentage } from "../../../utils"; import { getFeesInUSD } from "../../../utils/formatters"; import type { FeesBps } from "../types"; @@ -11,13 +12,19 @@ import type { FeesBps } from "../types"; export const useFees = ({ amount, feeConfigDto, + yieldFee, prices, token, }: { prices: Maybe; - token: Maybe; + token: Maybe; amount: BigNumber; feeConfigDto: Maybe; + yieldFee?: { + deposit?: string; + management?: string; + performance?: string; + } | null; }): { depositFee: Maybe; managementFee: Maybe; @@ -40,6 +47,21 @@ export const useFees = ({ [] ); + const getPercentAmount = useCallback( + (val: string) => amount.multipliedBy(val).dividedBy(100), + [amount] + ); + + const getPercentInUsd = useCallback( + (val: string) => + getFeesInUSD({ + amount: Just(getPercentAmount(val)), + prices, + token, + }), + [getPercentAmount, prices, token] + ); + const depositFee = useMemo( () => feeConfigDto @@ -49,8 +71,25 @@ export const useFees = ({ inPercentage: getBpsInPercentage(val), explanation: t("review.deposit_fee_explanation"), label: t("review.deposit_fee"), - })), - [feeConfigDto, getFeeInUSD, getBpsInPercentage, t] + })) + .altLazy(() => + Just(yieldFee?.deposit) + .chainNullable((v) => v) + .map((val) => ({ + inUSD: getPercentInUsd(val), + inPercentage: `${val}%`, + explanation: t("review.deposit_fee_explanation"), + label: t("review.deposit_fee"), + })) + ), + [ + feeConfigDto, + getFeeInUSD, + getBpsInPercentage, + getPercentInUsd, + t, + yieldFee, + ] ); const managementFee = useMemo( @@ -62,8 +101,25 @@ export const useFees = ({ inPercentage: getBpsInPercentage(val), explanation: t("review.management_fee_explanation"), label: t("review.management_fee"), - })), - [feeConfigDto, getFeeInUSD, getBpsInPercentage, t] + })) + .altLazy(() => + Just(yieldFee?.management) + .chainNullable((v) => v) + .map((val) => ({ + inUSD: getPercentInUsd(val), + inPercentage: `${val}%`, + explanation: t("review.management_fee_explanation"), + label: t("review.management_fee"), + })) + ), + [ + feeConfigDto, + getFeeInUSD, + getBpsInPercentage, + getPercentInUsd, + t, + yieldFee, + ] ); const performanceFee = useMemo( @@ -75,8 +131,25 @@ export const useFees = ({ inPercentage: getBpsInPercentage(val), explanation: t("review.performance_fee_explanation"), label: t("review.performance_fee"), - })), - [feeConfigDto, getFeeInUSD, getBpsInPercentage, t] + })) + .altLazy(() => + Just(yieldFee?.performance) + .chainNullable((v) => v) + .map((val) => ({ + inUSD: getPercentInUsd(val), + inPercentage: `${val}%`, + explanation: t("review.performance_fee_explanation"), + label: t("review.performance_fee"), + })) + ), + [ + feeConfigDto, + getFeeInUSD, + getBpsInPercentage, + getPercentInUsd, + t, + yieldFee, + ] ); return { depositFee, managementFee, performanceFee }; diff --git a/packages/widget/src/pages/review/hooks/use-pending-review.hook.ts b/packages/widget/src/pages/review/hooks/use-pending-review.hook.ts index c9a63cbb..415ebd80 100644 --- a/packages/widget/src/pages/review/hooks/use-pending-review.hook.ts +++ b/packages/widget/src/pages/review/hooks/use-pending-review.hook.ts @@ -1,23 +1,20 @@ -import { - type ActionTypes, - actionPending, - useActionPendingGasEstimate, -} from "@stakekit/api-hooks"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { useSelector } from "@xstate/store/react"; import BigNumber from "bignumber.js"; -import { EitherAsync, Maybe } from "purify-ts"; +import { Maybe } from "purify-ts"; import type { ComponentProps } from "react"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; import type { RewardTokenDetails } from "../../../components/molecules/reward-token-details"; -import { getValidStakeSessionTx } from "../../../domain"; import { useTokensPrices } from "../../../hooks/api/use-tokens-prices"; import { useGasWarningCheck } from "../../../hooks/use-gas-warning-check"; import { getRewardTokenSymbols } from "../../../hooks/use-reward-token-details/get-reward-token-symbols"; import { useSavedRef } from "../../../hooks/use-saved-ref"; import { usePendingActionStore } from "../../../providers/pending-action-store"; +import { useYieldApiFetchClient } from "../../../providers/yield-api-client-provider"; +import { createManageAction } from "../../../providers/yield-api-client-provider/actions"; +import type { YieldPendingActionType } from "../../../providers/yield-api-client-provider/types"; import { formatNumber } from "../../../utils"; import { getGasFeeInUSD } from "../../../utils/formatters"; import { useRegisterFooterButton } from "../../components/footer-outlet/context"; @@ -25,26 +22,44 @@ import type { MetaInfoProps } from "../pages/common-page/common.page"; export const usePendingActionReview = () => { const pendingActionStore = usePendingActionStore(); + const yieldApiFetchClient = useYieldApiFetchClient(); const pendingRequest = useSelector( pendingActionStore, (state) => state.context.data ).unsafeCoerce(); - const actionPendingGasEstimate = useActionPendingGasEstimate( - pendingRequest.requestDto, - { query: { staleTime: 0, gcTime: 0 } } - ); + const actionPreviewQuery = useQuery({ + enabled: !!pendingRequest, + queryKey: ["pending-review-action-preview", pendingRequest.requestDto], + retry: false, + queryFn: () => + createManageAction({ + addresses: pendingRequest.addresses, + fetchClient: yieldApiFetchClient, + requestDto: pendingRequest.requestDto, + yieldDto: pendingRequest.integrationData, + }), + }); const pendingTxGas = useMemo( () => - Maybe.fromNullable(actionPendingGasEstimate.data?.amount).map(BigNumber), - [actionPendingGasEstimate.data] + Maybe.fromNullable(actionPreviewQuery.data) + .map((actionDto) => + actionDto.transactions.reduce( + (acc, transaction) => + acc.plus(transaction.gasEstimate?.amount ?? 0), + new BigNumber(0) + ) + ) + .map((value) => (value.isZero() ? null : value)) + .chainNullable((value) => value), + [actionPreviewQuery.data] ); const amount = useMemo( - () => new BigNumber(pendingRequest.requestDto.args?.amount ?? 0), - [pendingRequest.requestDto.args?.amount] + () => new BigNumber(pendingRequest.requestDto.arguments?.amount ?? 0), + [pendingRequest.requestDto.arguments?.amount] ); const interactedToken = useMemo( @@ -77,11 +92,11 @@ export const usePendingActionReview = () => { Maybe.of( t( `position_details.pending_action_button.${ - pendingRequest.requestDto.type.toLowerCase() as Lowercase + pendingRequest.requestDto.action.toLowerCase() as Lowercase }` as const ) ), - [pendingRequest.requestDto.type, t] + [pendingRequest.requestDto.action, t] ); const navigate = useNavigate(); @@ -98,13 +113,9 @@ export const usePendingActionReview = () => { const actionPendingMutation = useMutation({ mutationFn: async () => - ( - await EitherAsync(() => actionPending(pendingRequest.requestDto)) - .mapLeft(() => new Error("Pending actions error")) - .chain((actionDto) => - EitherAsync.liftEither(getValidStakeSessionTx(actionDto)) - ) - ).unsafeCoerce(), + actionPreviewQuery.data ?? + (await actionPreviewQuery.refetch()).data ?? + Promise.reject(new Error("Pending actions error")), onSuccess: (data) => { pendingActionStore.send({ type: "setActionDto", data }); navigate("../steps", { relative: "path" }); @@ -131,11 +142,11 @@ export const usePendingActionReview = () => { return { type: "pendingAction", - pendingAction: pendingRequest.requestDto.type, + pendingAction: pendingRequest.requestDto.action, rewardToken, } satisfies ComponentProps; }), - [integrationData, pendingRequest.requestDto.type] + [integrationData, pendingRequest.requestDto.action] ); const onClickRef = useSavedRef(onClick); @@ -166,6 +177,8 @@ export const usePendingActionReview = () => { metaInfo, isGasCheckWarning: !!gasWarningCheck.data, gasCheckLoading: - actionPendingGasEstimate.isLoading || gasWarningCheck.isLoading, + actionPreviewQuery.isLoading || + actionPreviewQuery.isFetching || + gasWarningCheck.isLoading, }; }; diff --git a/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts b/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts index f400daba..da7f9d2e 100644 --- a/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts +++ b/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts @@ -1,17 +1,10 @@ -import { - actionEnter, - useActionEnterGasEstimation, - useYieldGetFeeConfiguration, -} from "@stakekit/api-hooks"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { useSelector } from "@xstate/store/react"; -import { isAxiosError } from "axios"; import BigNumber from "bignumber.js"; -import { EitherAsync, Maybe } from "purify-ts"; +import { Maybe } from "purify-ts"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; -import { getValidStakeSessionTx } from "../../../domain"; import { useTokensPrices } from "../../../hooks/api/use-tokens-prices"; import { useEstimatedRewards } from "../../../hooks/use-estimated-rewards"; import { useGasWarningCheck } from "../../../hooks/use-gas-warning-check"; @@ -20,6 +13,8 @@ import { useSavedRef } from "../../../hooks/use-saved-ref"; import { useYieldType } from "../../../hooks/use-yield-type"; import { useEnterStakeStore } from "../../../providers/enter-stake-store"; import { useSettings } from "../../../providers/settings"; +import { useYieldApiFetchClient } from "../../../providers/yield-api-client-provider"; +import { createEnterAction } from "../../../providers/yield-api-client-provider/actions"; import { APToPercentage, formatNumber } from "../../../utils"; import { getGasFeeInUSD } from "../../../utils/formatters"; import { useRegisterFooterButton } from "../../components/footer-outlet/context"; @@ -34,30 +29,47 @@ export const useStakeReview = () => { (state) => state.context.data ).unsafeCoerce(); - const integrationId = enterRequest.requestDto.integrationId; - const feeConfigDto = useYieldGetFeeConfiguration(integrationId); + const yieldApiFetchClient = useYieldApiFetchClient(); const stakeAmount = useMemo( - () => new BigNumber(enterRequest.requestDto.args.amount), + () => new BigNumber(enterRequest.requestDto.arguments?.amount ?? 0), [enterRequest] ); - const actionEnterGasEstimation = useActionEnterGasEstimation( - enterRequest.requestDto, - { query: { staleTime: 0, gcTime: 0 } } - ); + const actionPreviewQuery = useQuery({ + enabled: !!enterRequest, + queryKey: ["stake-review-action-preview", enterRequest.requestDto], + retry: false, + queryFn: () => + createEnterAction({ + addresses: enterRequest.addresses, + fetchClient: yieldApiFetchClient, + inputToken: enterRequest.selectedToken, + requestDto: enterRequest.requestDto, + yieldDto: enterRequest.selectedStake, + }), + }); const stakeEnterTxGas = useMemo( () => - Maybe.fromNullable(actionEnterGasEstimation.data?.amount).map(BigNumber), - [actionEnterGasEstimation.data] + Maybe.fromNullable(actionPreviewQuery.data) + .map((actionDto) => + actionDto.transactions.reduce( + (acc, transaction) => + acc.plus(transaction.gasEstimate?.amount ?? 0), + new BigNumber(0) + ) + ) + .map((value) => (value.isZero() ? null : value)) + .chainNullable((value) => value), + [actionPreviewQuery.data] ); const gasCheckWarning = useGasWarningCheck({ gasAmount: stakeEnterTxGas, gasFeeToken: enterRequest.gasFeeToken, - address: enterRequest.requestDto.addresses.address, - additionalAddresses: enterRequest.requestDto.addresses.additionalAddresses, + address: enterRequest.addresses.address, + additionalAddresses: enterRequest.addresses.additionalAddresses, isStake: true, stakeAmount, stakeToken: enterRequest.selectedToken, @@ -73,8 +85,8 @@ export const useStakeReview = () => { ); const selectedProviderYieldId = useMemo( - () => Maybe.fromNullable(enterRequest.requestDto.args.providerId), - [enterRequest.requestDto.args.providerId] + () => Maybe.fromNullable(enterRequest.requestDto.arguments?.providerId), + [enterRequest.requestDto.arguments?.providerId] ); const rewardToken = useRewardTokenDetails(selectedStake); @@ -113,9 +125,21 @@ export const useStakeReview = () => { const { depositFee, managementFee, performanceFee } = useFees({ amount: stakeAmount, token: selectedToken, - feeConfigDto: useMemo( - () => Maybe.fromNullable(feeConfigDto.data), - [feeConfigDto.data] + feeConfigDto: Maybe.empty(), + yieldFee: useMemo( + () => + ( + enterRequest.selectedStake as typeof enterRequest.selectedStake & { + mechanics?: { + fee?: { + deposit?: string; + management?: string; + performance?: string; + }; + }; + } + ).mechanics?.fee ?? null, + [enterRequest.selectedStake] ), prices: useMemo( () => Maybe.fromNullable(pricesState.data), @@ -129,24 +153,9 @@ export const useStakeReview = () => { const enterMutation = useMutation({ mutationFn: async () => - ( - await EitherAsync(() => actionEnter(enterRequest.requestDto)) - .mapLeft((e) => { - if ( - isAxiosError(e) && - StakingNotAllowedError.isStakingNotAllowedErrorDto( - e.response?.data - ) - ) { - return new StakingNotAllowedError(); - } - - return new Error("Stake enter error"); - }) - .chain((actionDto) => - EitherAsync.liftEither(getValidStakeSessionTx(actionDto)) - ) - ).unsafeCoerce(), + actionPreviewQuery.data ?? + (await actionPreviewQuery.refetch()).data ?? + Promise.reject(new Error("Stake enter error")), onSuccess: (data) => { enterStore.send({ type: "setActionDto", data }); navigate("/steps"); @@ -210,23 +219,13 @@ export const useStakeReview = () => { metaInfo, isGasCheckWarning: !!gasCheckWarning.data, gasCheckLoading: - actionEnterGasEstimation.isLoading || gasCheckWarning.isLoading, + actionPreviewQuery.isLoading || + actionPreviewQuery.isFetching || + gasCheckWarning.isLoading, depositFee, managementFee, performanceFee, - feeConfigLoading: feeConfigDto.isPending, + feeConfigLoading: actionPreviewQuery.isLoading, commissionFee, }; }; - -class StakingNotAllowedError extends Error { - static isStakingNotAllowedErrorDto = (e: unknown) => { - const dto = e as undefined | { type: string; code: number }; - - return dto && dto.code === 422 && dto.type === "STAKING_ERROR"; - }; - - constructor() { - super("Staking not allowed, needs unstaking and trying again"); - } -} diff --git a/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts b/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts index 974778b8..02e968e0 100644 --- a/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts +++ b/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts @@ -1,4 +1,4 @@ -import { useActionExitGasEstimate } from "@stakekit/api-hooks"; +import { useQuery } from "@tanstack/react-query"; import { useSelector } from "@xstate/store/react"; import BigNumber from "bignumber.js"; import { Maybe } from "purify-ts"; @@ -12,6 +12,8 @@ import { useGasWarningCheck } from "../../../hooks/use-gas-warning-check"; import { getRewardTokenSymbols } from "../../../hooks/use-reward-token-details/get-reward-token-symbols"; import { useSavedRef } from "../../../hooks/use-saved-ref"; import { useExitStakeStore } from "../../../providers/exit-stake-store"; +import { useYieldApiFetchClient } from "../../../providers/yield-api-client-provider"; +import { createExitAction } from "../../../providers/yield-api-client-provider/actions"; import { formatNumber } from "../../../utils"; import { getGasFeeInUSD } from "../../../utils/formatters"; import { useRegisterFooterButton } from "../../components/footer-outlet/context"; @@ -24,14 +26,34 @@ export const useUnstakeActionReview = () => { (state) => state.context.data ).unsafeCoerce(); - const actionExitGasEstimate = useActionExitGasEstimate( - exitRequest.requestDto, - { query: { staleTime: 0, gcTime: 0 } } - ); + const yieldApiFetchClient = useYieldApiFetchClient(); + + const actionPreviewQuery = useQuery({ + enabled: !!exitRequest, + queryKey: ["unstake-review-action-preview", exitRequest.requestDto], + retry: false, + queryFn: () => + createExitAction({ + addresses: exitRequest.addresses, + fetchClient: yieldApiFetchClient, + requestDto: exitRequest.requestDto, + yieldDto: exitRequest.integrationData, + }), + }); const stakeExitTxGas = useMemo( - () => Maybe.fromNullable(actionExitGasEstimate.data?.amount).map(BigNumber), - [actionExitGasEstimate.data] + () => + Maybe.fromNullable(actionPreviewQuery.data) + .map((actionDto) => + actionDto.transactions.reduce( + (acc, transaction) => + acc.plus(transaction.gasEstimate?.amount ?? 0), + new BigNumber(0) + ) + ) + .map((value) => (value.isZero() ? null : value)) + .chainNullable((value) => value), + [actionPreviewQuery.data] ); const interactedToken = useMemo( @@ -50,15 +72,15 @@ export const useUnstakeActionReview = () => { }); const amount = useMemo( - () => new BigNumber(exitRequest.requestDto.args.amount ?? 0), - [exitRequest.requestDto.args.amount] + () => new BigNumber(exitRequest.requestDto.arguments?.amount ?? 0), + [exitRequest.requestDto.arguments?.amount] ); const gasWarningCheck = useGasWarningCheck({ gasAmount: stakeExitTxGas, gasFeeToken: exitRequest.gasFeeToken, - address: exitRequest.requestDto.addresses.address, - additionalAddresses: exitRequest.requestDto.addresses.additionalAddresses, + address: exitRequest.addresses.address, + additionalAddresses: exitRequest.addresses.additionalAddresses, isStake: false, }); @@ -156,7 +178,9 @@ export const useUnstakeActionReview = () => { onCloseUnstakeSignMessage, showUnstakeSignMessagePopup, gasCheckLoading: - actionExitGasEstimate.isLoading || gasWarningCheck.isLoading, + actionPreviewQuery.isLoading || + actionPreviewQuery.isFetching || + gasWarningCheck.isLoading, isGasCheckWarning: !!gasWarningCheck.data, }; }; diff --git a/packages/widget/src/pages/review/pages/common-page/common.page.tsx b/packages/widget/src/pages/review/pages/common-page/common.page.tsx index 0e62d7c8..20569484 100644 --- a/packages/widget/src/pages/review/pages/common-page/common.page.tsx +++ b/packages/widget/src/pages/review/pages/common-page/common.page.tsx @@ -12,6 +12,7 @@ import { WarningBox } from "../../../../components/atoms/warning-box"; import type { RewardTokenDetails } from "../../../../components/molecules/reward-token-details"; import { useTrackEvent } from "../../../../hooks/tracking/use-track-event"; import { AnimationPage } from "../../../../navigation/containers/animation-page"; +import type { YieldTokenDto } from "../../../../providers/yield-api-client-provider/types"; import { MetaInfo } from "../../../components/meta-info"; import { PageContainer } from "../../../components/page-container"; import type { FeesBps } from "../../types"; @@ -25,7 +26,7 @@ export type MetaInfoProps = type ReviewPageProps = { fee: string; title: string; - token: Maybe; + token: Maybe; metadata: Maybe; info: ReactNode; rewardTokenDetailsProps: Maybe>; diff --git a/packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx b/packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx index fdfad48b..662d5406 100644 --- a/packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx +++ b/packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx @@ -8,11 +8,12 @@ import { TokenIcon } from "../../../../../components/atoms/token-icon"; import { Heading } from "../../../../../components/atoms/typography/heading"; import { Text } from "../../../../../components/atoms/typography/text"; import type { RewardTokenDetails } from "../../../../../components/molecules/reward-token-details"; +import type { YieldTokenDto } from "../../../../../providers/yield-api-client-provider/types"; import { headingStyles } from "../../style.css"; type Props = { title: string; - token: Maybe; + token: Maybe; metadata: Maybe; info: ReactNode; rewardTokenDetailsProps?: Maybe>; diff --git a/packages/widget/src/pages/steps/hooks/use-steps-machine.hook.ts b/packages/widget/src/pages/steps/hooks/use-steps-machine.hook.ts index f9fc8019..e081fe80 100644 --- a/packages/widget/src/pages/steps/hooks/use-steps-machine.hook.ts +++ b/packages/widget/src/pages/steps/hooks/use-steps-machine.hook.ts @@ -1,38 +1,28 @@ -import type { - ActionDto, - TransactionDto, - TransactionFormat, -} from "@stakekit/api-hooks"; -import { - transactionConstruct, - transactionGetTransaction, - transactionGetTransactionStatusFromId, - transactionSubmit, - transactionSubmitHash, -} from "@stakekit/api-hooks"; +import type { ActionDto, TransactionDto } from "@stakekit/api-hooks"; import { useMachine } from "@xstate/react"; -import { isAxiosError } from "axios"; import { EitherAsync, Left, List, Maybe, Right } from "purify-ts"; import { type RefObject, useMemo, useState } from "react"; import { assign, emit, setup } from "xstate"; -import { getAverageGasMode } from "../../../common/get-gas-mode-value"; -import { withRequestErrorRetry } from "../../../common/utils"; import { isTxError } from "../../../domain"; import type { ActionMeta } from "../../../domain/types/wallets/generic-wallet"; import { useTrackEvent } from "../../../hooks/tracking/use-track-event"; import { useSavedRef } from "../../../hooks/use-saved-ref"; -import { useSettings } from "../../../providers/settings"; import { useSKWallet } from "../../../providers/sk-wallet"; import type { SendTransactionError, TransactionDecodeError, } from "../../../providers/sk-wallet/errors"; +import { useYieldApiFetchClient } from "../../../providers/yield-api-client-provider"; +import { + getTransaction, + submitTransaction, + submitTransactionHash, +} from "../../../providers/yield-api-client-provider/actions"; import type { GetStakeSessionError } from "./errors"; import { SignError, SubmitError, SubmitHashError, - TransactionConstructError, TXCheckError, } from "./errors"; @@ -40,11 +30,7 @@ type TxMeta = { url: string | null; signedTx: string | null; broadcasted: boolean | null; - signError: - | SendTransactionError - | TransactionDecodeError - | TransactionConstructError - | null; + signError: SendTransactionError | TransactionDecodeError | null; txCheckError: GetStakeSessionError | null; done: boolean; }; @@ -73,24 +59,23 @@ export const useStepsMachine = ({ integrationId: ActionDto["integrationId"]; actionMeta: ActionMeta; }) => { - const { signTransaction, signMessage, isLedgerLive } = useSKWallet(); - const { preferredTransactionFormat } = useSettings(); + const { signTransaction, signMessage } = useSKWallet(); + const yieldApiFetchClient = useYieldApiFetchClient(); const trackEvent = useTrackEvent(); const sortedTransactions = useMemo( - () => transactions.sort((a, b) => a.stepIndex - b.stepIndex), + () => [...transactions].sort((a, b) => a.stepIndex - b.stepIndex), [transactions] ); const machineParams = useSavedRef({ transactions: sortedTransactions, integrationId, - isLedgerLive, trackEvent, signMessage, signTransaction, actionMeta, - preferredTransactionFormat, + yieldApiFetchClient, }); return useMachine(useState(() => getMachine(machineParams))[0]); @@ -101,22 +86,14 @@ const getMachine = ( RefObject<{ transactions: ActionDto["transactions"]; integrationId: ActionDto["integrationId"]; - isLedgerLive: boolean; trackEvent: ReturnType; signMessage: ReturnType["signMessage"]; signTransaction: ReturnType["signTransaction"]; actionMeta: ActionMeta; - preferredTransactionFormat?: TransactionFormat; + yieldApiFetchClient: ReturnType; }> > ) => { - const txConstruct = (...params: Parameters) => - withRequestErrorRetry({ - fn: () => transactionConstruct(...params), - shouldRetry: (e, retryCount) => - retryCount <= 3 && isAxiosError(e) && e.response?.status === 404, - }).mapLeft(() => new Error("Transaction construct error")); - const initContext = getInitContext( ref.current.transactions, ref.current.integrationId @@ -139,11 +116,7 @@ const getMachine = ( } | { type: "__SIGN_ERROR__"; - val: - | SendTransactionError - | TransactionDecodeError - | TransactionConstructError - | SignError; + val: SendTransactionError | TransactionDecodeError | SignError; } | { type: "__BROADCAST_SUCCESS__" } | { type: "__BROADCAST_ERROR__"; val: Error | SubmitHashError } @@ -237,94 +210,75 @@ const getMachine = ( EitherAsync.liftEither( context.currentTxMeta .chainNullable((v) => context.txStates[v.idx].tx) - .toEither(new TransactionConstructError("missing tx")) + .toEither(new SignError({ network: "unknown", txId: "unknown" })) ) .chain< - | TransactionConstructError - | SendTransactionError - | TransactionDecodeError - | SignError, + SendTransactionError | TransactionDecodeError | SignError, SignRes - >((tx) => - getAverageGasMode({ network: tx.network }) - .chainLeft(async () => Right(null)) - .chain((gas) => - txConstruct(tx.id, { - gasArgs: gas?.gasArgs, - ledgerWalletAPICompatible: ref.current.isLedgerLive, - ...(!!ref.current.preferredTransactionFormat && { - transactionFormat: ref.current.preferredTransactionFormat, - }), - }).mapLeft(() => new TransactionConstructError()) - ) - .chain< - | TransactionConstructError - | SendTransactionError - | TransactionDecodeError - | SignError, - SignRes - >((constructedTx) => { - if ( - constructedTx.status === "BROADCASTED" || - constructedTx.status === "CONFIRMED" - ) { - return EitherAsync.liftEither( - Right({ type: "broadcasted" }) - ); - } - - if (!constructedTx.unsignedTransaction) { - return EitherAsync.liftEither( - Left(new TransactionConstructError()) - ); - } - - if (constructedTx.isMessage) { - return ref.current - .signMessage(constructedTx.unsignedTransaction) - .map((val) => ({ - type: "regular" as const, - data: { signedTx: val, broadcasted: false }, - })) - .mapLeft( - () => - new SignError({ - network: constructedTx.network, - txId: constructedTx.id, - }) - ); - } + >((tx) => { + if (tx.status === "BROADCASTED" || tx.status === "CONFIRMED") { + return EitherAsync.liftEither(Right({ type: "broadcasted" })); + } - return ref.current - .signTransaction({ - tx: constructedTx.unsignedTransaction, - ledgerHwAppId: constructedTx.ledgerHwAppId, - txMeta: { - ...ref.current.actionMeta, - txId: constructedTx.id, - txType: constructedTx.type, - annotatedTransaction: - constructedTx.annotatedTransaction, - structuredTransaction: - constructedTx.structuredTransaction, - }, - network: constructedTx.network, + if (!tx.unsignedTransaction) { + return EitherAsync.liftEither( + Left( + new SignError({ + network: tx.network, + txId: tx.id, }) - .map((val) => ({ - ...val, - network: constructedTx.network, - txId: constructedTx.id, - })) - .ifRight(() => - ref.current.trackEvent("txSigned", { - txId: constructedTx.id, - network: constructedTx.network, - yieldId: context.yieldId, + ) + ); + } + + if (tx.isMessage) { + return ref.current + .signMessage(tx.unsignedTransaction) + .map((val) => ({ + type: "regular" as const, + data: { signedTx: val, broadcasted: false }, + })) + .mapLeft( + () => + new SignError({ + network: tx.network, + txId: tx.id, }) - ) - .map((val) => ({ type: "regular", data: val })); + ); + } + + const unsignedTransaction = + typeof tx.unsignedTransaction === "string" + ? tx.unsignedTransaction + : JSON.stringify(tx.unsignedTransaction); + + return ref.current + .signTransaction({ + tx: unsignedTransaction, + ledgerHwAppId: null, + txMeta: { + ...ref.current.actionMeta, + txId: tx.id, + txType: tx.type, + annotatedTransaction: tx.annotatedTransaction, + structuredTransaction: tx.structuredTransaction, + }, + network: tx.network, }) - ) + .map((val) => ({ + ...val, + network: tx.network, + txId: tx.id, + })) + .ifRight(() => + ref.current.trackEvent("txSigned", { + txId: tx.id, + network: tx.network, + yieldId: context.yieldId, + }) + ) + .map((val) => ({ type: "regular", data: val })); + }) .caseOf({ Left: (l) => { console.log(l); @@ -401,8 +355,10 @@ const getMachine = ( .chain((currentTx) => { if (currentTx.meta.broadcasted) { return EitherAsync(() => - transactionSubmitHash(currentTx.tx.id, { + submitTransactionHash({ + fetchClient: ref.current.yieldApiFetchClient, hash: currentTx.meta.signedTx!, + transactionId: currentTx.tx.id, }) ) .mapLeft(() => new SubmitHashError()) @@ -417,8 +373,10 @@ const getMachine = ( } return EitherAsync(() => - transactionSubmit(currentTx.tx.id, { + submitTransaction({ + fetchClient: ref.current.yieldApiFetchClient, signedTransaction: currentTx.meta.signedTx!, + transactionId: currentTx.tx.id, }) ) .mapLeft(() => new SubmitError()) @@ -490,23 +448,16 @@ const getMachine = ( .toEither(new Error("missing tx")) ) .chain((currentTx) => - withRequestErrorRetry({ - fn: () => - transactionGetTransactionStatusFromId(currentTx.tx.id), - shouldRetry: (e, retryCount) => - retryCount <= 3 && - isAxiosError(e) && - e.response?.status === 404, - }) - .map((res) => ({ url: res.url, status: res.status })) - .chainLeft(() => - EitherAsync(() => - transactionGetTransaction(currentTx.tx.id) - ).map((res) => ({ - url: res.explorerUrl, - status: res.status, - })) - ) + EitherAsync(() => + getTransaction({ + fetchClient: ref.current.yieldApiFetchClient, + transactionId: currentTx.tx.id, + }) + ) + .map((res) => ({ + url: res.explorerUrl, + status: res.status, + })) .mapLeft(() => new TXCheckError()) .chain((val) => EitherAsync.liftEither( @@ -549,7 +500,7 @@ const getMachine = ( ...val.meta, signError: null, txCheckError: null, - url: v.url, + url: v.url ?? null, done: true, }, } @@ -632,16 +583,28 @@ const getInitContext = ( const txStates = transactions.map((dto) => ({ tx: dto, meta: { - broadcasted: null, + broadcasted: + dto.status === "BROADCASTED" || dto.status === "CONFIRMED" + ? true + : null, signedTx: null, - url: null, + url: dto.explorerUrl ?? null, signError: null, txCheckError: null, - done: false, + done: dto.status === "CONFIRMED" || dto.status === "SKIPPED", }, })); - const currentTxIdx = 0; + const currentTxIdx = txStates.findIndex((txState) => !txState.meta.done); + + if (currentTxIdx === -1) { + return { + enabled: false, + txStates, + currentTxMeta: null, + yieldId: integrationId, + }; + } const currentTxMeta = { idx: currentTxIdx, diff --git a/packages/widget/src/providers/enter-stake-store/index.tsx b/packages/widget/src/providers/enter-stake-store/index.tsx index f1da4d09..c2a0912d 100644 --- a/packages/widget/src/providers/enter-stake-store/index.tsx +++ b/packages/widget/src/providers/enter-stake-store/index.tsx @@ -1,6 +1,6 @@ import type { ActionDto, - ActionRequestDto, + AddressesDto, TokenDto, ValidatorDto, YieldDto, @@ -8,9 +8,11 @@ import type { import { createStore } from "@xstate/store"; import { Maybe } from "purify-ts"; import { createContext, type PropsWithChildren, useContext } from "react"; +import type { YieldCreateActionDto } from "../yield-api-client-provider/types"; type InitData = { - requestDto: ActionRequestDto; + requestDto: YieldCreateActionDto; + addresses: AddressesDto; gasFeeToken: YieldDto["token"]; selectedStake: YieldDto; selectedValidators: Map; diff --git a/packages/widget/src/providers/exit-stake-store/index.tsx b/packages/widget/src/providers/exit-stake-store/index.tsx index 5542f1e9..44dde61b 100644 --- a/packages/widget/src/providers/exit-stake-store/index.tsx +++ b/packages/widget/src/providers/exit-stake-store/index.tsx @@ -1,6 +1,6 @@ import type { ActionDto, - ActionRequestDto, + AddressesDto, TokenDto, YieldDto, } from "@stakekit/api-hooks"; @@ -8,13 +8,18 @@ import { createStore } from "@xstate/store"; import type BigNumber from "bignumber.js"; import { Maybe } from "purify-ts"; import { createContext, type PropsWithChildren, useContext } from "react"; +import type { + YieldCreateActionDto, + YieldTokenDto, +} from "../yield-api-client-provider/types"; type InitData = { - requestDto: ActionRequestDto; + requestDto: YieldCreateActionDto; + addresses: AddressesDto; gasFeeToken: YieldDto["token"]; unstakeAmount: BigNumber; integrationData: YieldDto; - unstakeToken: TokenDto; + unstakeToken: TokenDto | YieldTokenDto; }; type Store = Maybe }>; diff --git a/packages/widget/src/providers/index.tsx b/packages/widget/src/providers/index.tsx index 10ab2017..796e8f67 100644 --- a/packages/widget/src/providers/index.tsx +++ b/packages/widget/src/providers/index.tsx @@ -29,6 +29,7 @@ import { ActionHistoryContextProvider } from "./stake-history"; import { ThemeWrapper } from "./theme-wrapper"; import { TrackingContextProviderWithProps } from "./tracking"; import { WagmiConfigProvider } from "./wagmi/provider"; +import { YieldApiClientProvider } from "./yield-api-client-provider"; export const Providers = ({ children, @@ -38,53 +39,55 @@ export const Providers = ({ - - - - - - - - - - - - - - - - - - - - - - - - {children} - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/widget/src/providers/pending-action-store/index.tsx b/packages/widget/src/providers/pending-action-store/index.tsx index 2350baff..13f179f0 100644 --- a/packages/widget/src/providers/pending-action-store/index.tsx +++ b/packages/widget/src/providers/pending-action-store/index.tsx @@ -1,21 +1,24 @@ import type { ActionDto, - ActionTypes, AddressesDto, - PendingActionRequestDto, TokenDto, YieldDto, } from "@stakekit/api-hooks"; import { createStore } from "@xstate/store"; import { Maybe } from "purify-ts"; import { createContext, type PropsWithChildren, useContext } from "react"; +import type { + YieldCreateManageActionDto, + YieldPendingActionType, + YieldTokenDto, +} from "../yield-api-client-provider/types"; type InitData = { - requestDto: PendingActionRequestDto; + requestDto: YieldCreateManageActionDto; addresses: AddressesDto; - pendingActionType: ActionTypes; + pendingActionType: YieldPendingActionType; integrationData: YieldDto; - interactedToken: TokenDto; + interactedToken: TokenDto | YieldTokenDto; gasFeeToken: TokenDto; }; diff --git a/packages/widget/src/providers/settings/types.ts b/packages/widget/src/providers/settings/types.ts index ba141092..b22e09d3 100644 --- a/packages/widget/src/providers/settings/types.ts +++ b/packages/widget/src/providers/settings/types.ts @@ -29,6 +29,7 @@ export type VariantProps = export type SettingsProps = { apiKey: string; baseUrl?: string; + yieldsApiUrl?: string; theme?: ThemeWrapperTheme; tracking?: { trackEvent?: (event: TrackEventVal, properties?: Properties) => void; diff --git a/packages/widget/src/providers/sk-wallet/errors.ts b/packages/widget/src/providers/sk-wallet/errors.ts index 2418f6b6..f3b4756b 100644 --- a/packages/widget/src/providers/sk-wallet/errors.ts +++ b/packages/widget/src/providers/sk-wallet/errors.ts @@ -11,8 +11,8 @@ export class SafeFailedError extends Error { export class SendTransactionError extends Error { _tag = "SendTransactionError"; - constructor(message?: string) { - super(message); + constructor(cause?: unknown) { + super("Send transaction failed", { cause }); this._tag = "SendTransactionError"; } @@ -20,8 +20,8 @@ export class SendTransactionError extends Error { export class TransactionDecodeError extends Error { _tag = "TransactionDecodeError"; - constructor(message?: string) { - super(message); + constructor(message?: string, cause?: unknown) { + super(message, { cause }); this._tag = "TransactionDecodeError"; } diff --git a/packages/widget/src/providers/sk-wallet/index.tsx b/packages/widget/src/providers/sk-wallet/index.tsx index 945f0a1f..8edc2543 100644 --- a/packages/widget/src/providers/sk-wallet/index.tsx +++ b/packages/widget/src/providers/sk-wallet/index.tsx @@ -453,7 +453,7 @@ export const SKWalletProvider = ({ children }: PropsWithChildren) => { type: val.maxFeePerGas ? "eip1559" : "legacy", }) ) - .mapLeft(() => new SendTransactionError()) + .mapLeft((e) => new SendTransactionError(e)) .map((val) => ({ signedTx: val, broadcasted: true })) ); }), diff --git a/packages/widget/src/providers/wagmi/index.ts b/packages/widget/src/providers/wagmi/index.ts index d8edb078..3b09cf5f 100644 --- a/packages/widget/src/providers/wagmi/index.ts +++ b/packages/widget/src/providers/wagmi/index.ts @@ -16,7 +16,8 @@ import { useQuery } from "@tanstack/react-query"; import uniqwith from "lodash.uniqwith"; import { createStore } from "mipd"; import { EitherAsync, Just, Left, Maybe, Right } from "purify-ts"; -import type { RefObject } from "react"; +import { type RefObject, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { createClient } from "viem"; import { createConfig, http } from "wagmi"; import type { Chain } from "wagmi/chains"; @@ -29,10 +30,8 @@ import type { EvmChainsMap } from "../../domain/types/chains/evm"; import type { MiscChainsMap } from "../../domain/types/chains/misc"; import type { SubstrateChainsMap } from "../../domain/types/chains/substrate"; import type { SKExternalProviders } from "../../domain/types/wallets"; -import type { ValidatorsConfig } from "../../domain/types/yields"; import { getInitParams } from "../../hooks/use-init-params"; import { useSavedRef } from "../../hooks/use-saved-ref"; -import { useValidatorsConfig } from "../../hooks/use-validators-config"; import type { GetEitherAsyncRight } from "../../types/utils"; import { isLedgerDappBrowserProvider } from "../../utils"; import { getEnabledNetworks } from "../api/get-enabled-networks"; @@ -46,6 +45,7 @@ import { getConfig as getSafeConnector } from "../safe/config"; import { useSettings } from "../settings"; import type { SettingsProps, VariantProps } from "../settings/types"; import { getConfig as getSubstrateConfig } from "../substrate/config"; +import { createYieldApiFetchClient } from "../yield-api-client-provider"; const mipdStore = createStore(); @@ -67,9 +67,9 @@ const buildWagmiConfig = async (opts: { forceWalletConnectOnly: boolean; customConnectors?: (chains: Chain[]) => WalletList; queryClient: QueryClient; + yieldApiFetchClient: ReturnType; isLedgerLive: boolean; isSafe: boolean; - validatorsConfig: ValidatorsConfig; chainIconMapping: SettingsProps["chainIconMapping"]; variant: VariantProps["variant"]; solanaWallets: SolanaWallet[]; @@ -113,8 +113,8 @@ const buildWagmiConfig = async (opts: { getInitParams({ isLedgerLive: opts.isLedgerLive, queryClient: opts.queryClient, + yieldApiFetchClient: opts.yieldApiFetchClient, externalProviders: opts.externalProviders?.current, - validatorsConfig: opts.validatorsConfig, }), ]).then(([evm, cosmos, misc, substrate, queryParams]) => evm.chain((e) => @@ -378,14 +378,24 @@ export const useWagmiConfig = () => { variant, mapWalletListFn, tonConnectManifestUrl, + apiKey, + yieldsApiUrl, } = useSettings(); + const { i18n } = useTranslation(); const solanaWallets = useSolanaWallet(); const solanaConnection = useSolanaConnection(); const queryClient = useSKQueryClient(); - - const validatorsConfig = useValidatorsConfig(); + const yieldApiFetchClient = useMemo( + () => + createYieldApiFetchClient({ + apiKey, + i18n, + url: yieldsApiUrl ?? config.env.yieldsApiUrl, + }), + [apiKey, i18n, yieldsApiUrl] + ); const externalProvidersRef = useSavedRef(externalProviders) as | RefObject @@ -401,12 +411,12 @@ export const useWagmiConfig = () => { forceWalletConnectOnly: !!wagmi?.forceWalletConnectOnly, customConnectors: wagmi?.__customConnectors__, queryClient, + yieldApiFetchClient, isLedgerLive: isLedgerDappBrowserProvider(), isSafe: !!isSafe, ...(externalProvidersRef.current && { externalProviders: externalProvidersRef, }), - validatorsConfig, chainIconMapping, variant, solanaWallets: solanaWallets.wallets, diff --git a/packages/widget/src/providers/yield-api-client-provider/actions.ts b/packages/widget/src/providers/yield-api-client-provider/actions.ts new file mode 100644 index 00000000..487a8538 --- /dev/null +++ b/packages/widget/src/providers/yield-api-client-provider/actions.ts @@ -0,0 +1,173 @@ +import type { + AddressesDto, + ActionDto as LegacyActionDto, + TokenDto, + YieldDto, +} from "@stakekit/api-hooks"; +import type { Client } from "openapi-fetch"; +import type { paths } from "../../types/yield-api-schema"; +import { adaptActionDto } from "./compat"; +import { getResponseData } from "./request-helpers"; +import type { YieldCreateActionDto, YieldCreateManageActionDto } from "./types"; + +export const createEnterAction = async ({ + addresses, + fetchClient, + inputToken, + requestDto, + yieldDto, +}: { + addresses: AddressesDto; + fetchClient: Client; + inputToken: TokenDto; + requestDto: YieldCreateActionDto; + yieldDto: YieldDto; +}): Promise => { + const actionDto = await getResponseData( + fetchClient.POST("/v1/actions/enter", { + body: requestDto, + }) + ); + + return adaptActionDto({ + actionDto, + addresses, + gasFeeToken: yieldDto.metadata.gasFeeToken, + inputToken, + yieldDto, + }); +}; + +export const createExitAction = async ({ + addresses, + fetchClient, + requestDto, + yieldDto, +}: { + addresses: AddressesDto; + fetchClient: Client; + requestDto: YieldCreateActionDto; + yieldDto: YieldDto; +}): Promise => { + const actionDto = await getResponseData( + fetchClient.POST("/v1/actions/exit", { + body: requestDto, + }) + ); + + return adaptActionDto({ + actionDto, + addresses, + gasFeeToken: yieldDto.metadata.gasFeeToken, + yieldDto, + }); +}; + +export const createManageAction = async ({ + addresses, + fetchClient, + requestDto, + yieldDto, +}: { + addresses: AddressesDto; + fetchClient: Client; + requestDto: YieldCreateManageActionDto; + yieldDto: YieldDto; +}): Promise => { + const actionDto = await getResponseData( + fetchClient.POST("/v1/actions/manage", { + body: requestDto, + }) + ); + + return adaptActionDto({ + actionDto, + addresses, + gasFeeToken: yieldDto.metadata.gasFeeToken, + yieldDto, + }); +}; + +export const listActions = async ({ + address, + fetchClient, + limit, + offset, +}: { + address: string; + fetchClient: Client; + limit: number; + offset: number; +}) => + getResponseData( + fetchClient.GET("/v1/actions", { + params: { + query: { + address, + offset, + limit, + }, + }, + }) + ); + +export const getTransaction = async ({ + fetchClient, + transactionId, +}: { + fetchClient: Client; + transactionId: string; +}) => + getResponseData( + fetchClient.GET("/v1/transactions/{transactionId}", { + params: { + path: { + transactionId, + }, + }, + }) + ); + +export const submitTransaction = async ({ + fetchClient, + signedTransaction, + transactionId, +}: { + fetchClient: Client; + signedTransaction: string; + transactionId: string; +}) => + getResponseData( + fetchClient.POST("/v1/transactions/{transactionId}/submit", { + params: { + path: { + transactionId, + }, + }, + body: { + signedTransaction, + }, + }) + ); + +export const submitTransactionHash = async ({ + fetchClient, + hash, + transactionId, +}: { + fetchClient: Client; + hash: string; + transactionId: string; +}) => + getResponseData( + fetchClient.PUT("/v1/transactions/{transactionId}/submit-hash", { + params: { + path: { + transactionId, + }, + }, + body: { + hash, + }, + }) + ); diff --git a/packages/widget/src/providers/yield-api-client-provider/compat.ts b/packages/widget/src/providers/yield-api-client-provider/compat.ts new file mode 100644 index 00000000..c290447b --- /dev/null +++ b/packages/widget/src/providers/yield-api-client-provider/compat.ts @@ -0,0 +1,520 @@ +import type { + AddressesDto, + ActionDto as LegacyActionDto, + TokenDto as LegacyTokenDto, + TransactionDto as LegacyTransactionDto, + ValidatorDto as LegacyValidatorDto, + YieldDto as LegacyYieldDto, +} from "@stakekit/api-hooks"; +import type { + YieldActionDto, + YieldDto, + YieldTokenDto, + YieldTransactionDto, + YieldValidatorDto, +} from "./types"; + +const NATIVE_TOKEN_PLACEHOLDER = "0x"; + +const toLower = (value: string) => value.toLowerCase(); + +type EncodedGasEstimate = { + amount?: string | null; + gasLimit?: string | null; + token?: YieldTokenDto | LegacyTokenDto | null; +}; + +const mapToken = ( + token: YieldTokenDto | LegacyTokenDto | null | undefined +): LegacyTokenDto | undefined => { + if (!token) return undefined; + + return { ...token } as LegacyTokenDto; +}; + +const uniqTokens = ( + tokens: (YieldTokenDto | LegacyTokenDto | null | undefined)[] +) => { + const seen = new Set(); + + return tokens.flatMap((token) => { + const mapped = mapToken(token); + + if (!mapped) return []; + + const key = `${mapped.network}:${mapped.address?.toLowerCase() ?? ""}:${ + mapped.symbol + }`; + + if (seen.has(key)) { + return []; + } + + seen.add(key); + return [mapped]; + }); +}; + +const secondsToDays = (seconds: number | undefined) => { + if (seconds === undefined) return undefined; + + return { days: Math.round(seconds / 86400) }; +}; + +const getRewardType = ({ + yieldDto, + legacyYieldDto, +}: { + yieldDto: YieldDto; + legacyYieldDto: LegacyYieldDto | null; +}): LegacyYieldDto["rewardType"] => { + const rateType = yieldDto.rewardRate?.rateType?.toLowerCase(); + + if (rateType === "apr" || rateType === "apy") { + return rateType; + } + + return legacyYieldDto?.rewardType ?? "variable"; +}; + +const getArgumentConfig = ( + yieldDto: YieldDto, + legacyYieldDto: LegacyYieldDto | null, + type: "enter" | "exit" +) => { + const fields = yieldDto.mechanics?.arguments?.[type]?.fields ?? []; + const legacyArgs = legacyYieldDto?.args?.[type]?.args ?? {}; + const nextArgs = { ...legacyArgs } as Record; + + for (const field of fields) { + const legacyField = (legacyArgs as Record)[field.name]; + const common = { + required: !!field.required, + ...(field.minimum !== undefined && field.minimum !== null + ? { minimum: Number(field.minimum) } + : {}), + ...(field.maximum !== undefined && field.maximum !== null + ? { maximum: Number(field.maximum) } + : {}), + ...(field.options ? { options: field.options } : {}), + }; + + nextArgs[field.name] = + legacyField && + typeof legacyField === "object" && + !Array.isArray(legacyField) + ? { + ...legacyField, + ...common, + } + : common; + } + + return { + ...(legacyYieldDto?.args?.[type] ?? {}), + args: nextArgs, + }; +}; + +const getRewardTokens = ({ + yieldDto, + legacyYieldDto, +}: { + yieldDto: YieldDto; + legacyYieldDto: LegacyYieldDto | null; +}) => { + if (legacyYieldDto?.metadata.rewardTokens?.length) { + return legacyYieldDto.metadata.rewardTokens; + } + + const seen = new Set(); + const derived = uniqTokens( + yieldDto.rewardRate?.components?.map((component) => component.token) ?? [] + ).filter((token) => { + const key = `${token.network}:${token.address?.toLowerCase() ?? ""}`; + + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); + + if (derived.length) { + return derived; + } + + return legacyYieldDto?.metadata.rewardTokens; +}; + +const getMetadata = ({ + yieldDto, + legacyYieldDto, +}: { + yieldDto: YieldDto; + legacyYieldDto: LegacyYieldDto | null; +}): LegacyYieldDto["metadata"] => { + const fallbackMetadata = legacyYieldDto?.metadata; + const mechanics = yieldDto.mechanics; + const metadata = yieldDto.metadata; + const yieldTypeMap = { + staking: "staking", + restaking: "restaking", + lending: "lending", + vault: "vault", + liquidity_pool: "vault", + concentrated_liquidity_pool: "vault", + fixed_yield: "vault", + real_world_asset: "vault", + } as const; + const type = + fallbackMetadata?.type ?? + yieldTypeMap[(mechanics?.type ?? "vault") as keyof typeof yieldTypeMap] ?? + "vault"; + + const token = mapToken(yieldDto.token) ?? fallbackMetadata?.token; + const tokens = uniqTokens([ + ...(yieldDto.tokens ?? []), + ...(yieldDto.inputTokens ?? []), + ...(fallbackMetadata?.tokens ?? []), + ]); + const gasFeeToken = + mapToken(mechanics?.gasFeeToken) ?? fallbackMetadata?.gasFeeToken; + + return { + ...(fallbackMetadata ?? {}), + name: metadata?.name ?? fallbackMetadata?.name ?? "", + description: metadata?.description ?? fallbackMetadata?.description ?? "", + documentation: + metadata?.documentation ?? fallbackMetadata?.documentation ?? "", + logoURI: metadata?.logoURI ?? fallbackMetadata?.logoURI ?? "", + type, + token: + token ?? + fallbackMetadata?.token ?? + (mapToken(yieldDto.token) as LegacyTokenDto), + tokens: tokens.length ? tokens : fallbackMetadata?.tokens, + rewardTokens: getRewardTokens({ yieldDto, legacyYieldDto }), + rewardSchedule: + mechanics?.rewardSchedule ?? fallbackMetadata?.rewardSchedule, + rewardClaiming: + mechanics?.rewardClaiming ?? fallbackMetadata?.rewardClaiming, + cooldownPeriod: + secondsToDays(mechanics?.cooldownPeriod?.seconds) ?? + fallbackMetadata?.cooldownPeriod, + warmupPeriod: + secondsToDays(mechanics?.warmupPeriod?.seconds) ?? + fallbackMetadata?.warmupPeriod, + gasFeeToken: + gasFeeToken ?? + fallbackMetadata?.gasFeeToken ?? + (mapToken(yieldDto.token) as LegacyTokenDto), + supportsLedgerWalletApi: + mechanics?.supportsLedgerWalletApi ?? + fallbackMetadata?.supportsLedgerWalletApi, + supportsMultipleValidators: + mechanics?.requiresValidatorSelection ?? + fallbackMetadata?.supportsMultipleValidators, + supportedStandards: + metadata?.supportedStandards ?? fallbackMetadata?.supportedStandards, + fee: { + enabled: !!mechanics?.fee || !!fallbackMetadata?.fee?.enabled, + depositFee: + !!mechanics?.fee?.deposit || !!fallbackMetadata?.fee?.depositFee, + managementFee: + !!mechanics?.fee?.management || !!fallbackMetadata?.fee?.managementFee, + performanceFee: + !!mechanics?.fee?.performance || + !!fallbackMetadata?.fee?.performanceFee, + }, + } as LegacyYieldDto["metadata"]; +}; + +export const adaptValidatorDto = ( + validatorDto: YieldValidatorDto | LegacyValidatorDto +): LegacyValidatorDto => { + const legacyValidator = validatorDto as LegacyValidatorDto; + const rewardRate = + "rewardRate" in validatorDto ? validatorDto.rewardRate : undefined; + const providerId = + "provider" in validatorDto + ? (validatorDto.provider?.id ?? validatorDto.providerId) + : validatorDto.providerId; + const image = + "logoURI" in validatorDto ? validatorDto.logoURI : legacyValidator.image; + const stakedBalance = + "tvl" in validatorDto ? validatorDto.tvl : legacyValidator.stakedBalance; + + return { + address: validatorDto.address, + apr: "apr" in validatorDto ? validatorDto.apr : rewardRate?.total, + commission: validatorDto.commission, + image, + minimumStake: validatorDto.minimumStake, + name: validatorDto.name, + nominatorCount: validatorDto.nominatorCount, + preferred: validatorDto.preferred, + pricePerShare: validatorDto.pricePerShare, + providerId, + remainingPossibleStake: validatorDto.remainingPossibleStake, + remainingSlots: validatorDto.remainingSlots, + stakedBalance, + status: validatorDto.status as LegacyValidatorDto["status"], + subnetId: validatorDto.subnetId, + subnetName: + "subnetName" in validatorDto ? validatorDto.subnetName : undefined, + tokenSymbol: validatorDto.tokenSymbol, + votingPower: validatorDto.votingPower, + website: validatorDto.website, + }; +}; + +export const adaptYieldDto = ({ + yieldDto, + legacyYieldDto, +}: { + yieldDto: YieldDto; + legacyYieldDto: LegacyYieldDto | null; +}): LegacyYieldDto => { + const { validators: _legacyValidators, ...legacyYieldDtoWithoutValidators } = + legacyYieldDto ?? {}; + const rewardRate = + yieldDto.rewardRate?.total ?? legacyYieldDto?.rewardRate ?? 0; + const tokens = uniqTokens([ + ...(yieldDto.tokens ?? []), + ...(yieldDto.inputTokens ?? []), + ...(legacyYieldDto?.tokens ?? []), + ]); + + const token = + mapToken(yieldDto.token) ?? + legacyYieldDto?.token ?? + (tokens[0] as LegacyTokenDto); + + return { + ...legacyYieldDtoWithoutValidators, + id: yieldDto.id, + token, + tokens: tokens.length ? tokens : (legacyYieldDto?.tokens ?? [token]), + metadata: getMetadata({ yieldDto, legacyYieldDto }), + rewardRate, + rewardRateDetails: yieldDto.rewardRate, + rewardType: getRewardType({ yieldDto, legacyYieldDto }), + status: { + ...(legacyYieldDto?.status ?? {}), + ...(yieldDto.status ?? {}), + }, + args: { + ...(legacyYieldDto?.args ?? {}), + enter: getArgumentConfig(yieldDto, legacyYieldDto, "enter"), + ...(yieldDto.mechanics?.arguments?.exit || legacyYieldDto?.args?.exit + ? { + exit: getArgumentConfig(yieldDto, legacyYieldDto, "exit"), + } + : {}), + } as LegacyYieldDto["args"], + feeConfigurations: legacyYieldDto?.feeConfigurations ?? [], + apy: rewardRate, + inputTokens: yieldDto.inputTokens, + outputToken: yieldDto.outputToken, + network: yieldDto.network, + chainId: yieldDto.chainId, + mechanics: yieldDto.mechanics, + statistics: yieldDto.statistics, + risk: yieldDto.risk, + providerId: yieldDto.providerId, + tags: yieldDto.tags, + state: yieldDto.state, + curator: yieldDto.curator, + } as unknown as LegacyYieldDto; +}; + +const getCurrentStepIndex = (transactions: LegacyTransactionDto[]) => { + const idx = transactions.findIndex( + (transaction) => + transaction.status !== "CONFIRMED" && transaction.status !== "SKIPPED" + ); + + if (idx >= 0) { + return idx; + } + + return Math.max(transactions.length - 1, 0); +}; + +export const toActionInputToken = ({ + inputToken, + yieldDto, + inputTokenValue, +}: { + inputToken?: LegacyTokenDto; + yieldDto?: LegacyYieldDto | null; + inputTokenValue?: string | null; +}) => { + if (inputToken) { + return inputToken; + } + + if (!yieldDto) { + return undefined; + } + + if (!inputTokenValue) { + return yieldDto.token ?? yieldDto.tokens?.[0]; + } + + const needle = toLower(inputTokenValue); + + return ( + [ + yieldDto.token, + ...(yieldDto.tokens ?? []), + ...(yieldDto.metadata.tokens ?? []), + ].find((token) => { + const address = token.address ? toLower(token.address) : null; + return ( + address === needle || + token.symbol.toLowerCase() === needle || + (needle === NATIVE_TOKEN_PLACEHOLDER && !token.address) + ); + }) ?? + yieldDto.token ?? + yieldDto.tokens?.[0] + ); +}; + +export const adaptTransactionDto = ({ + transactionDto, + gasFeeToken, + stakeId, +}: { + transactionDto: YieldTransactionDto; + gasFeeToken?: LegacyTokenDto; + stakeId: string; +}): LegacyTransactionDto => ({ + id: transactionDto.id, + accountAddresses: undefined, + annotatedTransaction: (transactionDto.annotatedTransaction ?? + {}) as unknown as LegacyTransactionDto["annotatedTransaction"], + broadcastedAt: transactionDto.broadcastedAt, + createdAt: transactionDto.createdAt, + error: transactionDto.error ?? null, + explorerUrl: transactionDto.explorerUrl ?? null, + gasEstimate: parseGasEstimate(transactionDto.gasEstimate, gasFeeToken), + hash: transactionDto.hash, + isMessage: transactionDto.isMessage ?? false, + ledgerHwAppId: null, + network: transactionDto.network as LegacyTransactionDto["network"], + signedTransaction: transactionDto.signedTransaction, + stakeId, + status: transactionDto.status as LegacyTransactionDto["status"], + stepIndex: transactionDto.stepIndex ?? 0, + structuredTransaction: (transactionDto.structuredTransaction ?? + {}) as unknown as LegacyTransactionDto["structuredTransaction"], + type: transactionDto.type as LegacyTransactionDto["type"], + unsignedTransaction: + typeof transactionDto.unsignedTransaction === "string" + ? transactionDto.unsignedTransaction + : transactionDto.unsignedTransaction + ? JSON.stringify(transactionDto.unsignedTransaction) + : null, +}); + +const parseGasEstimate = ( + gasEstimate: YieldTransactionDto["gasEstimate"], + gasFeeToken?: LegacyTokenDto +): LegacyTransactionDto["gasEstimate"] => { + if (!gasEstimate) { + return null; + } + + try { + const parsed = JSON.parse(gasEstimate) as EncodedGasEstimate | null; + + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + if (!gasFeeToken) { + return null; + } + + return { + amount: gasEstimate, + token: gasFeeToken, + }; + } + + const amount = parsed.amount ?? null; + const token = mapToken(parsed.token) ?? gasFeeToken; + + if (!amount || !token) { + return null; + } + + return { + amount, + token, + ...(parsed.gasLimit ? { gasLimit: parsed.gasLimit } : {}), + }; + } catch { + if (!gasFeeToken) { + return null; + } + + return { + amount: gasEstimate, + token: gasFeeToken, + }; + } +}; + +export const adaptActionDto = ({ + actionDto, + addresses, + gasFeeToken, + inputToken, + yieldDto, +}: { + actionDto: YieldActionDto; + addresses?: AddressesDto; + gasFeeToken?: LegacyTokenDto; + inputToken?: LegacyTokenDto; + yieldDto?: LegacyYieldDto | null; +}): LegacyActionDto => { + const adaptedTransactions = actionDto.transactions.map((transactionDto) => + adaptTransactionDto({ + transactionDto, + gasFeeToken: + gasFeeToken ?? yieldDto?.metadata.gasFeeToken ?? yieldDto?.token, + stakeId: actionDto.yieldId, + }) + ); + + const rawArguments = actionDto.rawArguments; + const validatorAddresses = + rawArguments?.validatorAddresses ?? + (rawArguments?.validatorAddress ? [rawArguments.validatorAddress] : null); + + return { + id: actionDto.id, + accountAddresses: undefined, + addresses: addresses ?? { address: actionDto.address }, + amount: actionDto.amount, + completedAt: actionDto.completedAt, + createdAt: actionDto.createdAt, + currentStepIndex: getCurrentStepIndex(adaptedTransactions), + inputToken: toActionInputToken({ + inputToken, + yieldDto, + inputTokenValue: rawArguments?.inputToken, + }), + integrationId: actionDto.yieldId, + projectId: null, + status: actionDto.status, + tokenId: rawArguments?.tokenId ?? null, + transactions: adaptedTransactions, + type: actionDto.type as LegacyActionDto["type"], + USDAmount: actionDto.amountUsd, + validatorAddress: rawArguments?.validatorAddress ?? null, + validatorAddresses, + }; +}; diff --git a/packages/widget/src/providers/yield-api-client-provider/index.tsx b/packages/widget/src/providers/yield-api-client-provider/index.tsx new file mode 100644 index 00000000..b3bb82df --- /dev/null +++ b/packages/widget/src/providers/yield-api-client-provider/index.tsx @@ -0,0 +1,124 @@ +import type { i18n } from "i18next"; +import type { Client } from "openapi-fetch"; +import createFetchClient from "openapi-fetch"; +import type { OpenapiQueryClient } from "openapi-react-query"; +import createClient from "openapi-react-query"; +import type { PropsWithChildren } from "react"; +import { createContext, useContext, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { waitForDelayedApiRequests } from "../../common/delay-api-requests"; +import { config } from "../../config"; +import { handleGeoBlockResponse } from "../../hooks/use-geo-block"; +import { handleRichErrorResponse } from "../../hooks/use-rich-errors"; +import type { paths } from "../../types/yield-api-schema"; +import { useSettings } from "../settings"; + +const QueryContext = createContext | undefined>( + undefined +); +const FetchContext = createContext | undefined>(undefined); + +export const createYieldApiFetchClient = ({ + apiKey, + i18n, + url, +}: { + apiKey: string; + i18n?: i18n; + url: string; +}) => { + const client = createFetchClient({ + baseUrl: url, + headers: { + "x-api-key": apiKey, + }, + }); + + client.use({ + onResponse: async ({ request, response }) => { + await waitForDelayedApiRequests(); + + if (!response.ok) { + const data = await readYieldErrorResponse(response); + + handleGeoBlockResponse({ + data, + status: response.status, + }); + + if (i18n) { + handleRichErrorResponse({ + data, + i18n, + url: request.url, + }); + } + } + + return response; + }, + }); + + return client; +}; + +const readYieldErrorResponse = async (response: Response) => { + const text = await response.clone().text(); + + if (!text) { + return undefined; + } + + try { + return JSON.parse(text); + } catch { + return text; + } +}; + +export const YieldApiClientProvider = ({ children }: PropsWithChildren) => { + const { apiKey, yieldsApiUrl } = useSettings(); + const { i18n } = useTranslation(); + const url = yieldsApiUrl ?? config.env.yieldsApiUrl; + + const clients = useMemo(() => { + const fetchClient = createYieldApiFetchClient({ apiKey, i18n, url }); + + return { + fetchClient, + queryClient: createClient(fetchClient), + }; + }, [apiKey, i18n, url]); + + return ( + + + {children} + + + ); +}; + +export const useYieldApiClient = () => { + const value = useContext(QueryContext); + + if (!value) { + throw new Error( + "useYieldApiClient must be used within a YieldApiClientProvider" + ); + } + + return value; +}; + +export const useYieldApiFetchClient = () => { + const value = useContext(FetchContext); + + if (!value) { + throw new Error( + "useYieldApiFetchClient must be used within a YieldApiClientProvider" + ); + } + + return value; +}; diff --git a/packages/widget/src/providers/yield-api-client-provider/request-helpers.ts b/packages/widget/src/providers/yield-api-client-provider/request-helpers.ts new file mode 100644 index 00000000..6fbe7630 --- /dev/null +++ b/packages/widget/src/providers/yield-api-client-provider/request-helpers.ts @@ -0,0 +1,34 @@ +import type { AddressWithTokenDtoAdditionalAddresses } from "@stakekit/api-hooks"; +import type { YieldActionArgumentsDto } from "./types"; + +export const withAdditionalAddresses = ({ + additionalAddresses, + argumentsDto, +}: { + additionalAddresses: + | AddressWithTokenDtoAdditionalAddresses + | null + | undefined; + argumentsDto: YieldActionArgumentsDto; +}) => + ({ + ...argumentsDto, + ...(additionalAddresses ?? {}), + }) satisfies YieldActionArgumentsDto; + +export const getResponseData = async < + TResponse extends { + data?: unknown; + error?: unknown; + }, +>( + promise: Promise +): Promise> => { + const response = await promise; + + if (response.data !== undefined && response.data !== null) { + return response.data; + } + + throw response.error ?? new Error("Yield API request failed"); +}; diff --git a/packages/widget/src/providers/yield-api-client-provider/types.ts b/packages/widget/src/providers/yield-api-client-provider/types.ts new file mode 100644 index 00000000..750288f7 --- /dev/null +++ b/packages/widget/src/providers/yield-api-client-provider/types.ts @@ -0,0 +1,36 @@ +import type { components } from "../../types/yield-api-schema"; + +export type YieldDto = components["schemas"]["YieldDto"]; +export type YieldBalancesRequestDto = + components["schemas"]["BalancesRequestDto"]; +export type YieldSingleBalancesRequestDto = + components["schemas"]["YieldBalancesRequestDto"]; +export type YieldBalanceType = components["schemas"]["BalanceType"]; + +export type YieldPendingActionDto = components["schemas"]["PendingActionDto"]; +export type YieldTokenDto = components["schemas"]["TokenDto"]; +export type YieldRewardDto = components["schemas"]["RewardDto"]; +export type YieldRewardRateDto = components["schemas"]["RewardRateDto"]; +export type YieldActionArgumentsDto = + components["schemas"]["ActionArgumentsDto"]; +export type YieldCreateActionDto = components["schemas"]["CreateActionDto"]; +export type YieldCreateManageActionDto = + components["schemas"]["CreateManageActionDto"]; +export type YieldPendingActionType = + | YieldPendingActionDto["type"] + | NonNullable; + +export type YieldValidatorDto = components["schemas"]["ValidatorDto"]; + +export type YieldBalanceDto = components["schemas"]["BalanceDto"]; +export type YieldActionDto = components["schemas"]["ActionDto"]; +export type YieldTransactionDto = components["schemas"]["TransactionDto"]; + +export type YieldBalancesByYieldDto = components["schemas"]["YieldBalancesDto"]; +export type YieldSingleBalancesResponseDto = + components["schemas"]["YieldBalancesDto"]; + +export type YieldBalancesResponseDto = + components["schemas"]["BalancesResponseDto"]; +export type YieldPaginatedResponseDto = + components["schemas"]["PaginatedResponseDto"]; diff --git a/packages/widget/src/translation/English/translations.json b/packages/widget/src/translation/English/translations.json index f4f13e8d..5af0f911 100644 --- a/packages/widget/src/translation/English/translations.json +++ b/packages/widget/src/translation/English/translations.json @@ -157,7 +157,14 @@ "validators_nominator_count": "Nominator count", "validators_subnet_name": "Subnet name", "validators_market_cap": "Market cap", - "validators_token_symbol": "Token symbol" + "validators_token_symbol": "Token symbol", + "apy_composition": { + "title": "APY composition", + "native": "Native APY", + "protocol_incentive": "Protocol incentive APY", + "campaign": "Campaign APY", + "up_to": "Up to {{value}}" + } }, "dashboard": { "details": { @@ -328,6 +335,8 @@ "stake": "staked", "unstake": "unstaked", "claim_rewards": "claimed rewards", + "auto_sweep_unstake_rewards": "unstaked rewards", + "auto_sweep_withdraw_rewards": "withdrew rewards", "restake_rewards": "restaked rewards", "withdraw": "withdrawn", "restake": "restaked", @@ -445,6 +454,7 @@ "unstake": "Unstake", "withdraw": "Withdraw", "available": "{{amount}} {{symbol}} available", + "personalized_apy": "Personalized APY", "select_validators": { "submit": "Submit" }, @@ -463,6 +473,15 @@ "restaking": "Unstake" }, "balance_type": { + "active": "Active", + "active_yearn_or_deposit": "Deposited", + "entering": "Entering", + "entering_yearn_or_deposit": "Depositing", + "exiting": "Exiting", + "exiting_yearn_or_deposit": "Withdrawing", + "withdrawable": "Withdrawable", + "withdrawable_yearn_or_deposit": "Available to withdraw", + "claimable": "Claimable", "available": "Available", "staked": "Staked", "staked_yearn_or_deposit": "Deposited", @@ -481,6 +500,8 @@ "stake": "Stake", "unstake": "Unstake", "claim_rewards": "Claim rewards", + "auto_sweep_unstake_rewards": "Unstake rewards", + "auto_sweep_withdraw_rewards": "Withdraw rewards", "restake_rewards": "Restake rewards", "withdraw": "Withdraw", "withdraw_all": "Withdraw all", @@ -509,6 +530,8 @@ "stake": "Stake", "unstake": "Unstake", "claim_rewards": "Claim", + "auto_sweep_unstake_rewards": "Unstake rewards", + "auto_sweep_withdraw_rewards": "Withdraw rewards", "restake_rewards": "Restake", "withdraw": "Withdraw", "withdraw_all": "Withdraw all", @@ -540,6 +563,8 @@ "stake": "Stake with {{providerName}}", "unstake": "Unstake from {{providerName}}", "claim_rewards": "Claim rewards from {{providerName}}", + "auto_sweep_unstake_rewards": "Unstake rewards from {{providerName}}", + "auto_sweep_withdraw_rewards": "Withdraw rewards from {{providerName}}", "restake_rewards": "Restake rewards with {{providerName}}", "withdraw": "Withdraw from {{providerName}}", "withdraw_all": "Withdraw all from {{providerName}}", diff --git a/packages/widget/src/translation/French/translations.json b/packages/widget/src/translation/French/translations.json index 0ca0efac..1fd96c1a 100644 --- a/packages/widget/src/translation/French/translations.json +++ b/packages/widget/src/translation/French/translations.json @@ -154,7 +154,14 @@ "validators_nominator_count": "Nombre de nominators", "validators_subnet_name": "Nom du sous-réseau", "validators_market_cap": "Capitalisation boursière", - "validators_token_symbol": "Symbole du token" + "validators_token_symbol": "Symbole du token", + "apy_composition": { + "title": "Composition de l'APY", + "native": "APY natif", + "protocol_incentive": "APY d'incitation protocolaire", + "campaign": "APY de campagne", + "up_to": "Jusqu'a {{value}}" + } }, "review": { "estimated_reward": "Récompenses estimées", @@ -278,6 +285,8 @@ "stake": "staké", "unstake": "déstaké", "claim_rewards": "récompenses réclamées", + "auto_sweep_unstake_rewards": "récompenses retirées", + "auto_sweep_withdraw_rewards": "récompenses retirées", "restake_rewards": "récompenses restakées", "withdraw": "reitré", "restake": "restaké", @@ -395,6 +404,7 @@ "unstake": "Déstaker", "withdraw": "Retirer", "available": "{{amount}} {{symbol}} disponibles", + "personalized_apy": "APY personnalise", "select_validators": { "submit": "Envoyer" }, @@ -413,6 +423,15 @@ "restaking": "Déstaker" }, "balance_type": { + "active": "Actif", + "active_yearn_or_deposit": "Déposé", + "entering": "En cours d'entrée", + "entering_yearn_or_deposit": "En cours de dépôt", + "exiting": "En cours de sortie", + "exiting_yearn_or_deposit": "En cours de retrait", + "withdrawable": "Retirable", + "withdrawable_yearn_or_deposit": "Disponible au retrait", + "claimable": "Réclamable", "available": "Disponible", "staked": "Staké", "staked_yearn_or_deposit": "Déposé", @@ -431,6 +450,8 @@ "stake": "Staker", "unstake": "Déstaker", "claim_rewards": "Réclamer les récompenses", + "auto_sweep_unstake_rewards": "Retirer les récompenses", + "auto_sweep_withdraw_rewards": "Retirer les récompenses", "restake_rewards": "Restaker les récompenses", "withdraw": "Retirer", "withdraw_all": "Retirer tout", @@ -459,6 +480,8 @@ "stake": "Staker", "unstake": "Déstaker", "claim_rewards": "Réclamer", + "auto_sweep_unstake_rewards": "Retirer les récompenses", + "auto_sweep_withdraw_rewards": "Retirer les récompenses", "restake_rewards": "Restaker", "withdraw": "Retirer", "withdraw_all": "Retirer tout", @@ -490,6 +513,8 @@ "stake": "Staker avec {{providerName}}", "unstake": "Déstaker de {{providerName}}", "claim_rewards": "Réclamer les récompenses de {{providerName}}", + "auto_sweep_unstake_rewards": "Retirer les récompenses de {{providerName}}", + "auto_sweep_withdraw_rewards": "Retirer les récompenses de {{providerName}}", "restake_rewards": "Restaker les récompenses avec {{providerName}}", "withdraw": "Retirer de {{providerName}}", "withdraw_all": "Retirer tout de {{providerName}}", diff --git a/packages/widget/src/types/yield-api-schema.d.ts b/packages/widget/src/types/yield-api-schema.d.ts new file mode 100644 index 00000000..6ed192e2 --- /dev/null +++ b/packages/widget/src/types/yield-api-schema.d.ts @@ -0,0 +1,5995 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/v1/yields": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all yield opportunities + * @description Retrieve a paginated list of available yield opportunities across all supported networks and protocols. + */ + get: operations["YieldsController_getYields"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/balances": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get balances across multiple yields and networks + * @description Retrieve balances for multiple wallet addresses across different networks and yield opportunities. Send an array of balance requests - each request can specify a yieldId (optional for chain scanning), address, network, and custom arguments. This is the same format as the single yield balance endpoint but in array form. Duplicate requests (same yieldId + address + network) are automatically deduplicated, with specific yield requests taking precedence over chain scans. + */ + post: operations["YieldsController_getAggregateBalances"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/{yieldId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get yield metadata + * @description Retrieve detailed information about a specific yield opportunity including APY, tokens, protocol details, and more. + */ + get: operations["YieldsController_getYield"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/{yieldId}/risk": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get risk metadata for a yield + * @description Retrieve risk metadata associated with a specific yield. + */ + get: operations["YieldsController_getYieldRisk"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/{yieldId}/balances/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get historical balance snapshots for a yield + * @description Returns a chronological time series of balance snapshots for a wallet address within a yield. Each entry reflects the position at a specific timestamp or block. Supports configurable sampling intervals and point-in-time queries. Only available for ERC4626 vaults with indexed transfer history. + */ + get: operations["YieldsController_getBalanceHistory"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/{yieldId}/balances": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get balances for a specific yield + * @description Retrieve all balances associated with a yield opportunity for a specific wallet address, including active, pending, claimable, and withdrawable balances. The network is automatically determined from the yield configuration. + */ + post: operations["YieldsController_getYieldBalances"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/{yieldId}/rewards/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get reward history + * @description Retrieve a chronological list of on-chain reward events for an indexed yield. Each record includes timestamp, token metadata, amount, reward source, and transaction reference. + */ + get: operations["YieldsController_getYieldRewards"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/{yieldId}/reward-rate/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get historical reward rate snapshots for a yield + * @description Returns a chronological time series of reward rate snapshots for the specified yield, suitable for charting and analytics. Supports configurable time ranges, sampling intervals (day/week/month), and pagination. + */ + get: operations["YieldsController_getYieldRewardRateHistory"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/{yieldId}/tvl/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get historical TVL snapshots for a yield + * @description Returns a chronological time series of Total Value Locked for the specified yield, expressed in underlying token units. Supports configurable time ranges, sampling intervals (day/week/month), and pagination. + */ + get: operations["YieldsController_getYieldTvlHistory"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/yields/{yieldId}/validators": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get yield validators + * @description Retrieve a paginated list of validators available for staking or delegation for this yield opportunity. + */ + get: operations["YieldsController_getYieldValidators"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/actions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get user actions + * @description Retrieve all actions performed by a user, with optional filtering by yield, status, category, etc. In the future, this may include personalized action recommendations. + */ + get: operations["ActionsController_getActions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/actions/{actionId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get action details + * @description Retrieve detailed information about a specific action including current status, transactions, and execution details. + */ + get: operations["ActionsController_getAction"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/actions/enter": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Enter a yield + * @description Generate the transactions needed to enter a yield position with the provided parameters. + */ + post: operations["ActionsController_enterYield"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/actions/exit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Exit a yield + * @description Generate the transactions needed to exit a yield position with the provided parameters. + */ + post: operations["ActionsController_exitYield"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/actions/manage": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Manage a yield + * @description Generate the transactions needed to perform management actions on a yield position. + */ + post: operations["ActionsController_manageYield"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transactions/{transactionId}/submit-hash": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Submit transaction hash + * @description Submit the transaction hash after broadcasting a transaction to the blockchain. This updates the transaction status and enables tracking. + */ + put: operations["TransactionsController_submitTransactionHash"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transactions/{transactionId}/submit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit transaction + * @description Submit the transaction to the blockchain. + */ + post: operations["TransactionsController_submitTransaction"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transactions/{transactionId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get transaction details + * @description Retrieve detailed information about a specific transaction including current status, hash, and execution details. + */ + get: operations["TransactionsController_getTransaction"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/networks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all available networks + * @description Retrieve a list of all supported networks that can be used for filtering yields and other operations. + */ + get: operations["NetworksController_getNetworks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/providers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all providers + * @description Returns a paginated list of all providers, including both protocol and validator providers. + */ + get: operations["ProvidersController_getProviders"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/providers/{providerId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get provider by ID + * @description Returns detailed information about a specific provider. + */ + get: operations["ProvidersController_getProvider"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health check + * @description Get the health status of the yield API with current timestamp + */ + get: operations["HealthController_health"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + TokenDto: { + /** + * @description Token symbol + * @example ETH + */ + symbol: string; + /** + * @description Token name + * @example Ethereum + */ + name: string; + /** + * @description Token decimal places + * @example 18 + */ + decimals: number; + /** + * @description Token network + * @example Ethereum + * @enum {string} + */ + network: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description Token address (if applicable) + * @example 0x... + */ + address?: string; + /** + * @description Token logo URI + * @example https://... + */ + logoURI?: string; + /** + * @description Token is points + * @example true + */ + isPoints?: boolean; + /** + * @description Token CoinGecko ID + * @example ethereum + */ + coinGeckoId?: string; + }; + RewardDto: { + /** + * @description Reward rate as a decimal (e.g. 0.04 = 4%) + * @example 0.04 + */ + rate: number; + /** + * @description Whether this rate is APR or APY + * @example APR + */ + rateType: string; + /** @description Token received as reward */ + token: components["schemas"]["TokenDto"]; + /** + * @description Structured source of yield (e.g. staking, protocol incentive) + * @example protocol_incentive + * @enum {string} + */ + yieldSource: + | "staking" + | "restaking" + | "protocol_incentive" + | "campaign_incentive" + | "points" + | "lending_interest" + | "mev" + | "real_world_asset_yield" + | "vault"; + /** + * @description Optional human-readable description of this reward + * @example LDO distributed to incentivize stETH adoption via Lido Boost + */ + description?: string; + }; + RewardRateDto: { + /** + * @description Estimated reward rate across all sources (e.g. staking, points) + * @example 6.5 + */ + total: number; + /** + * @description Whether this reward rate is APR or APY + * @example APR + */ + rateType: string; + /** @description Breakdown of reward rates by source */ + components: components["schemas"]["RewardDto"][]; + }; + YieldStatisticsDto: { + /** + * @description Total value locked in USD + * @example 1,200,000 + */ + tvlUsd?: string | null; + /** + * @description Total value locked in primary underlying token + * @example 500.25 + */ + tvl?: string | null; + /** + * @description Raw total value locked (full precision) + * @example 500250000000000000000 + */ + tvlRaw?: string | null; + /** + * @description Number of users with active positions in the yield + * @example 348 + */ + uniqueUsers?: number | null; + /** + * @description Average position size in USD + * @example 3,448.27 + */ + averagePositionSizeUsd?: string | null; + /** + * @description Average position size in primary underlying token + * @example 1.44 + */ + averagePositionSize?: string | null; + }; + YieldRiskExponentialDto: { + /** @description Exponential pool rating */ + poolRating?: Record; + /** @description Exponential pool score (1-5) */ + poolScore?: Record; + /** @description Exponential rating description */ + ratingDescription?: Record; + /** @description Exponential pool URL */ + url?: Record; + }; + YieldRiskCredoraDto: { + /** @description Credora rating */ + rating?: Record; + /** @description Credora score (1-5) */ + score?: Record; + /** @description Probability of Significant Loss (annualized) */ + psl?: Record; + /** @description Credora publish date */ + publishDate?: Record; + /** @description Credora curator name */ + curator?: Record; + }; + YieldRiskDto: { + /** @description Risk data last update timestamp */ + updatedAt: string; + exponentialFi?: components["schemas"]["YieldRiskExponentialDto"]; + credora?: components["schemas"]["YieldRiskCredoraDto"]; + }; + YieldStatusDto: { + /** + * @description Whether the user can currently enter this yield + * @example true + */ + enter: boolean; + /** + * @description Whether the user can currently exit this yield + * @example true + */ + exit: boolean; + }; + /** + * @description Supported standards for this yield + * @enum {string} + */ + ERCStandards: "ERC20" | "ERC4626" | "ERC721" | "ERC1155"; + YieldMetadataDto: { + /** + * @description Display name of the yield opportunity + * @example Lido Staking + */ + name: string; + /** + * @description Yield opportunity logo URI + * @example https://lido.fi/logo.png + */ + logoURI: string; + /** + * @description Markdown-supported short description of this yield opportunity, including where rewards come from. + * @example Stake ETH with Lido to earn auto-compounding validator rewards via stETH. + */ + description: string; + /** + * @description Link to documentation or integration guide + * @example https://docs.lido.fi + */ + documentation: string; + /** + * @description Whether this yield is currently under maintenance + * @example false + */ + underMaintenance: boolean; + /** + * @description Whether this yield is deprecated and will be discontinued + * @example false + */ + deprecated: boolean; + supportedStandards: components["schemas"]["ERCStandards"][]; + }; + /** + * @description Type of yield mechanism (staking, restaking, LP, vault, etc.) + * @enum {string} + */ + YieldType: + | "staking" + | "restaking" + | "lending" + | "vault" + | "fixed_yield" + | "real_world_asset" + | "concentrated_liquidity_pool" + | "liquidity_pool"; + /** + * @description How often rewards are distributed (e.g. continuously, epoch-based) + * @enum {string} + */ + RewardSchedule: + | "block" + | "hour" + | "day" + | "week" + | "month" + | "era" + | "epoch" + | "campaign"; + /** + * @description How rewards are claimed: auto, manual, or mixed + * @enum {string} + */ + RewardClaiming: "auto" | "manual"; + TimePeriodDto: { + /** + * @description Duration in seconds + * @example 86400 + */ + seconds: number; + }; + YieldFeeDto: { + /** + * @description Deposit fee percentage + * @example 0.00 + */ + deposit?: string; + /** + * @description Withdrawal fee percentage + * @example 0.00 + */ + withdrawal?: string; + /** + * @description Management fee percentage (annual) + * @example 2.00 + */ + management?: string; + /** + * @description Performance fee percentage + * @example 20.00 + */ + performance?: string; + }; + YieldEntryLimitsDto: { + /** + * @description Minimum amount required to enter this yield in token units (null if no minimum) + * @example 0.01 + */ + minimum: string | null; + /** + * @description Maximum amount allowed to enter this yield in token units (null if no limit) + * @example 1000.0 + */ + maximum: string | null; + }; + YieldRequirementsDto: { + /** @description Whether off-chain KYC is required before transacting */ + kycRequired: boolean; + /** @description Issuer's KYC portal URL */ + kycUrl?: string; + }; + ArgumentFieldDto: { + /** + * @description Field name + * @example amount + * @enum {string} + */ + name: + | "amount" + | "amounts" + | "validatorAddress" + | "validatorAddresses" + | "receiverAddress" + | "providerId" + | "duration" + | "inputToken" + | "inputTokenNetwork" + | "outputToken" + | "outputTokenNetwork" + | "subnetId" + | "tronResource" + | "feeConfigurationId" + | "cosmosPubKey" + | "tezosPubKey" + | "cAddressBech" + | "pAddressBech" + | "executionMode" + | "ledgerWalletApiCompatible" + | "useMaxAmount" + | "useInstantExecution" + | "rangeMin" + | "rangeMax" + | "percentage" + | "tokenId" + | "skipPrechecks"; + /** + * @description Field type + * @example string + * @enum {string} + */ + type: "string" | "number" | "address" | "enum" | "boolean"; + /** + * @description Field label + * @example Amount to Enter + */ + label: string; + /** @description Field description */ + description?: string; + /** + * @description Whether the field is required + * @example true + */ + required?: boolean; + /** + * @description Options for enum fields + * @example [ + * "individual", + * "batched" + * ] + */ + options?: string[]; + /** + * @description Reference to API endpoint that provides options dynamically + * @example /api/v1/validators?integrationId=eth-lido + */ + optionsRef?: string; + /** @description Default value for the field */ + default?: Record; + /** @description Placeholder text for the field */ + placeholder?: string; + /** + * @description Minimum allowed value for number fields (null if no minimum) + * @example 1.0 + */ + minimum?: string | null; + /** + * @description Maximum allowed value for number fields (null if no maximum) + * @example 100.0 + */ + maximum?: string | null; + /** + * @description Whether the field is an array + * @example false + */ + isArray?: boolean; + }; + ArgumentSchemaDto: { + /** @description List of argument fields */ + fields: components["schemas"]["ArgumentFieldDto"][]; + /** @description Notes or instructions for these arguments */ + notes?: string; + }; + YieldMechanicsArgumentsDto: { + enter?: components["schemas"]["ArgumentSchemaDto"]; + exit?: components["schemas"]["ArgumentSchemaDto"]; + /** @description Manage action schemas. Each yield supports different ActionTypes (CLAIM_UNSTAKED, CLAIM_REWARDS, etc.). Keys are ActionTypes enum values. */ + manage?: { + [key: string]: components["schemas"]["ArgumentSchemaDto"]; + }; + /** @description Arguments for the balances endpoint (e.g., alternative addresses, chain-specific fields) */ + balance?: components["schemas"]["ArgumentSchemaDto"]; + }; + PossibleFeeTakingMechanismsDto: { + /** + * @description User can take (earn) a deposit fee + * @example false + */ + depositFee: boolean; + /** + * @description User can take (earn) a management fee + * @example false + */ + managementFee: boolean; + /** + * @description User can take (earn) a performance fee + * @example false + */ + performanceFee: boolean; + /** + * @description User can take (earn) validator rebates + * @example false + */ + validatorRebates: boolean; + }; + YieldMechanicsDto: { + type: components["schemas"]["YieldType"]; + /** + * @description Indicates whether this yield requires validator selection + * @example true + */ + requiresValidatorSelection?: boolean; + rewardSchedule: components["schemas"]["RewardSchedule"]; + rewardClaiming: components["schemas"]["RewardClaiming"]; + /** @description Token used for gas fees (typically native) */ + gasFeeToken: components["schemas"]["TokenDto"]; + /** @description Lockup period - minimum time before exit can be initiated */ + lockupPeriod?: components["schemas"]["TimePeriodDto"]; + /** @description Cooldown period required before exit is allowed */ + cooldownPeriod?: components["schemas"]["TimePeriodDto"]; + /** @description Warmup period before rewards start accruing */ + warmupPeriod?: components["schemas"]["TimePeriodDto"]; + /** @description Fees charged to the user for this yield (e.g., deposit, management, performance). */ + fee?: components["schemas"]["YieldFeeDto"]; + /** @description Entry amount limits for this yield */ + entryLimits?: components["schemas"]["YieldEntryLimitsDto"]; + /** @description Access requirements (e.g. KYC) for this yield */ + requirements?: components["schemas"]["YieldRequirementsDto"]; + /** @description Supports Ledger Wallet API (connect via Ledger Live) */ + supportsLedgerWalletApi?: boolean; + /** @description Additional transaction formats supported (e.g. safe, batch) */ + extraTransactionFormatsSupported?: ("raw" | "default")[]; + /** @description Arguments required for each action (enter, exit, manage, etc.) */ + arguments?: components["schemas"]["YieldMechanicsArgumentsDto"]; + /** @description Possible fee-taking mechanisms for the user or integrator (i.e., what fees the user/integrator can potentially earn from this yield). */ + possibleFeeTakingMechanisms?: components["schemas"]["PossibleFeeTakingMechanismsDto"]; + }; + CuratorDto: { + /** @description Curator name */ + name?: Record | null; + /** @description Curator description */ + description?: Record | null; + /** @description Curator logo URI */ + logoURI?: Record | null; + }; + PricePerShareStateDto: { + /** + * @description Price per share for the yield (e.g., LP token price, vault share price) + * @example 1.05 + */ + price: number; + /** @description Share token (the token you own shares of) */ + shareToken: components["schemas"]["TokenDto"]; + /** @description Quote token (the token the price is denominated in) */ + quoteToken: components["schemas"]["TokenDto"]; + }; + ConcentratedLiquidityPoolStateDto: { + /** + * @description Full-range trading APR (24h or rolling) + * @example 0.12 + */ + baseApr: number; + /** + * @description Current mid-price from the AMM (token1 per token0) + * @example 3950.42 + */ + price: number; + /** + * @description Tick spacing required so UI can snap ranges + * @example 50 + */ + tickSpacing: number; + /** + * @description Minimum tick bound for the pool + * @example -887272 + */ + minTick: number; + /** + * @description Maximum tick bound for the pool + * @example 887272 + */ + maxTick: number; + /** + * @description 24-hour trading volume in USD + * @example 149550871.99 + */ + volume24hUsd: number | null; + /** + * @description 24-hour fees earned by LPs in USD + * @example 14955.09 + */ + fee24hUsd: number | null; + /** + * @description Total value locked in USD + * @example 9213550.2 + */ + tvlUsd: number | null; + /** + * @description Pool fee tier as a decimal (e.g., 0.0005 for 0.05%) + * @example 0.0005 + */ + feeTier: number; + /** @description Base token (token0) */ + baseToken: components["schemas"]["TokenDto"]; + /** @description Quote token (token1) */ + quoteToken: components["schemas"]["TokenDto"]; + }; + CapacityDto: { + /** @description Current total assets in the yield */ + current: string; + /** @description Maximum capacity of the yield */ + max?: string | null; + /** @description Remaining capacity available for deposits */ + remaining?: string | null; + }; + LiquidityStateDto: { + /** + * @description Available liquidity in underlying token units + * @example 250000.00 + */ + liquidity?: Record | null; + /** + * @description Utilization rate as a decimal (e.g., 0.8 = 80%) + * @example 0.80 + */ + utilization?: Record | null; + }; + AllocationRewardRateDto: { + /** + * @description Total reward rate + * @example 5.25 + */ + total: number; + /** + * @description Whether this rate is APR or APY + * @example APY + */ + rateType: string; + }; + AllocationDto: { + /** + * @description Contract address of the underlying strategy + * @example 0x1234567890abcdef1234567890abcdef12345678 + */ + address: string; + /** + * @description Network the underlying strategy is on + * @example base + * @enum {string} + */ + network: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description Display name of the underlying strategy + * @example Morpho Moonwell USDC + */ + name: string; + /** + * @description Yield ID if this strategy is supported as a separate yield opportunity + * @example base-usdc-morpho-moonwell-usdc + */ + yieldId?: string; + /** + * @description Provider ID for this strategy (e.g., morpho, aave, lido) + * @example morpho + */ + providerId?: string; + /** + * @description Amount allocated to this strategy in input token units + * @example 50000.00 + */ + allocation: string; + /** + * @description USD value of the allocation + * @example 50000.00 + */ + allocationUsd: string | null; + /** + * @description Current weight of this strategy as a percentage (0-100) + * @example 50.5 + */ + weight: number; + /** + * @description Target weight of this strategy as a percentage (0-100) + * @example 50 + */ + targetWeight: number; + /** @description Reward rate of the underlying strategy */ + rewardRate: components["schemas"]["AllocationRewardRateDto"] | null; + /** + * @description Total value locked in the underlying strategy in input token units + * @example 500.25 + */ + tvl: string | null; + /** + * @description Total value locked in USD for the underlying strategy + * @example 10000000.00 + */ + tvlUsd: string | null; + /** + * @description Maximum capacity of the underlying strategy + * @example 1000000.00 + */ + maxCapacity: string | null; + /** + * @description Remaining capacity in the underlying strategy + * @example 500000.00 + */ + remainingCapacity: string | null; + }; + YieldStateDto: { + /** @description Price per share state metadata */ + pricePerShareState?: components["schemas"]["PricePerShareStateDto"]; + /** @description Concentrated liquidity pool state metadata */ + concentratedLiquidityPoolState?: components["schemas"]["ConcentratedLiquidityPoolStateDto"]; + /** @description Capacity state metadata */ + capacityState?: components["schemas"]["CapacityDto"]; + /** @description Liquidity state (available liquidity, utilization rate) */ + liquidityState?: components["schemas"]["LiquidityStateDto"]; + /** @description Allocations to underlying strategies for vault yields (e.g., OAV, Morpho). Includes allocation, APY, TVL, and capacity per strategy. */ + allocations?: components["schemas"]["AllocationDto"][]; + }; + YieldDto: { + /** + * @description Unique identifier for this yield opportunity + * @example ethereum-eth-lido-staking + */ + id: string; + /** + * @description Network this yield opportunity is on + * @enum {string} + */ + network: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description EVM chain ID for this network (only for EVM networks) + * @example 1 + */ + chainId?: string; + /** @description Accepted input tokens (auto-converted as needed) */ + inputTokens: components["schemas"]["TokenDto"][]; + /** @description Token received from the protocol */ + outputToken?: components["schemas"]["TokenDto"]; + /** @description Canonical deposit token - used for balances */ + token: components["schemas"]["TokenDto"]; + /** @description Canonical deposit tokens - used for balances */ + tokens: components["schemas"]["TokenDto"][]; + /** @description Total effective yield broken down by source and token. */ + rewardRate: components["schemas"]["RewardRateDto"]; + /** @description Key statistics and analytics for this yield opportunity */ + statistics?: components["schemas"]["YieldStatisticsDto"]; + /** @description Risk scores and provider ratings for this yield */ + risk?: components["schemas"]["YieldRiskDto"]; + /** @description Current availability of user actions like enter, exit, claim */ + status: components["schemas"]["YieldStatusDto"]; + /** @description Descriptive metadata including name, logo, description, and documentation */ + metadata: components["schemas"]["YieldMetadataDto"]; + /** @description Operational mechanics including constraints, fees, and capabilities */ + mechanics: components["schemas"]["YieldMechanicsDto"]; + /** + * @description The provider ID this yield belongs to + * @example lido + */ + providerId: string; + /** @description Curator information for the yield (if applicable) */ + curator?: components["schemas"]["CuratorDto"]; + /** + * @description Optional tags for filtering or categorization + * @example [ + * "restaking", + * "ETH", + * "LST" + * ] + */ + tags?: string[]; + /** @description Dynamic, real-time protocol-level state values that affect entering or exiting a yield (e.g., pool price, capacity, price per share, liquidity, queue depth) */ + state?: components["schemas"]["YieldStateDto"]; + }; + /** + * @description Type of balance + * @enum {string} + */ + BalanceType: + | "active" + | "entering" + | "exiting" + | "withdrawable" + | "claimable" + | "locked"; + PendingActionDto: { + /** + * @description High-level action intent + * @example manage + * @enum {string} + */ + intent: "enter" | "manage" | "exit"; + /** + * @description Specific action type + * @example CLAIM_REWARDS + * @enum {string} + */ + type: + | "STAKE" + | "UNSTAKE" + | "CLAIM_REWARDS" + | "AUTO_SWEEP_UNSTAKE_REWARDS" + | "AUTO_SWEEP_WITHDRAW_REWARDS" + | "RESTAKE_REWARDS" + | "WITHDRAW" + | "WITHDRAW_ALL" + | "RESTAKE" + | "CLAIM_UNSTAKED" + | "UNLOCK_LOCKED" + | "STAKE_LOCKED" + | "VOTE" + | "REVOKE" + | "VOTE_LOCKED" + | "REVOTE" + | "REBOND" + | "MIGRATE" + | "VERIFY_WITHDRAW_CREDENTIALS" + | "DELEGATE"; + /** + * @description Server-generated passthrough that must be included when executing the action + * @example eyJhZGRyZXNzZXMiOnsiYWRkcmVzcyI6ImNvc21vczF5ZXk... + */ + passthrough: string; + /** @description Argument schema required to execute this action */ + arguments?: components["schemas"]["ArgumentSchemaDto"] | null; + /** + * @description Amount involved in the action, in human-readable token units (not the smallest denomination). + * @example 0.1 + */ + amount?: string | null; + }; + RevShareDetailsDto: { + /** + * @description Minimum revenue share percentage (0-1) + * @example 0.3 + */ + minRevShare: number; + /** + * @description Maximum revenue share percentage (0-1) + * @example 0.7 + */ + maxRevShare: number; + }; + RevShareTiersDto: { + /** @description Trial tier revenue share details */ + trial?: components["schemas"]["RevShareDetailsDto"]; + /** @description Standard tier revenue share details */ + standard?: components["schemas"]["RevShareDetailsDto"]; + /** @description Pro tier revenue share details */ + pro?: components["schemas"]["RevShareDetailsDto"]; + }; + ValidatorProviderDto: { + /** + * @description Provider name + * @example Morpho + */ + name: string; + /** + * @description Provider ID + * @example morpho + */ + id: string; + /** + * @description Provider logo URI + * @example https://morpho.xyz/logo.png + */ + logoURI: string; + /** + * @description Short description of the provider + * @example A peer-to-peer DeFi lending protocol + */ + description: string; + /** + * @description Provider website + * @example https://morpho.xyz + */ + website: string; + /** + * @description Total TVL across the entire provider in USD + * @example 10,200,000 + */ + tvlUsd: Record | null; + /** + * @description Type of provider (protocol or validator provider) + * @example protocol + * @enum {string} + */ + type: "protocol" | "validator_provider"; + /** @description Optional social/media references or audit links */ + references?: string[] | null; + /** + * @description Provider ranking (lower numbers indicate higher preference) + * @example 1 + */ + rank: number; + /** + * @description Whether this provider is marked as preferred + * @example true + */ + preferred: boolean; + /** + * @description Revenue sharing details by tier + * @example { + * "standard": { + * "minRevShare": 0.3, + * "maxRevShare": 0.7 + * }, + * "pro": { + * "minRevShare": 0.4, + * "maxRevShare": 0.8 + * } + * } + */ + revshare?: components["schemas"]["RevShareTiersDto"]; + /** + * @deprecated + * @description Provider ID (deprecated, use `id` instead) + * @example luganodes + */ + uniqueId?: string; + /** + * Format: date-time + * @deprecated + * @description Creation timestamp (deprecated) + */ + createdAt?: string; + /** + * Format: date-time + * @deprecated + * @description Last update timestamp (deprecated) + */ + updatedAt?: string; + }; + ValidatorDto: { + /** + * @description Validator address or ID + * @example cosmosvaloper1abc... + */ + address: string; + /** + * @description Validator display name + * @example StakeKit Validator + */ + name?: string; + /** + * @description Validator logo URI + * @example https://stakekit.com/logo.png + */ + logoURI?: string; + /** + * @description Link to validator website + * @example https://stakekit.com + */ + website?: string; + /** + * @description Detailed reward rate breakdown by source (emissions, MEV, fees, etc.) + * @example { + * "total": 8.4, + * "rateType": "APR", + * "components": [ + * { + * "rate": 6.8, + * "rateType": "APR", + * "token": { + * "symbol": "SOL", + * "name": "Solana" + * }, + * "yieldSource": "staking", + * "description": "Solana network inflation rewards" + * }, + * { + * "rate": 1.2, + * "rateType": "APR", + * "token": { + * "symbol": "SOL", + * "name": "Solana" + * }, + * "yieldSource": "validator_commission", + * "description": "Transaction fees from processed transactions" + * }, + * { + * "rate": 0.4, + * "rateType": "APR", + * "token": { + * "symbol": "SOL", + * "name": "Solana" + * }, + * "yieldSource": "mev", + * "description": "MEV from Jito block space auctions" + * } + * ] + * } + */ + rewardRate?: components["schemas"]["RewardRateDto"]; + /** @description Provider information for this validator */ + provider?: components["schemas"]["ValidatorProviderDto"]; + /** + * @description Commission rate charged by validator + * @example 0.05 + */ + commission?: number; + /** + * @description Total value locked with this validator in USD + * @example 18,340,000 + */ + tvlUsd?: string; + /** + * @description Total value locked with this validator in native token + * @example 8250.45 + */ + tvl?: string; + /** + * @description Raw total value locked with this validator (full precision) + * @example 8250450000000000000000 + */ + tvlRaw?: string; + /** + * @description Validator's voting power share (0–1) + * @example 0.013 + */ + votingPower?: number; + /** + * @description Whether this validator is flagged as preferred + * @example true + */ + preferred?: boolean; + /** + * @description Minimum stake allowed in native token + * @example 1.0 + */ + minimumStake?: string; + /** + * @description Maximum available stake before hitting cap in native token + * @example 285,714.28 + */ + remainingPossibleStake?: string; + /** + * @description Number of remaining nominator/delegator slots (for capped chains) + * @example 8 + */ + remainingSlots?: number; + /** + * @description Number of current nominators + * @example 321 + */ + nominatorCount?: number; + /** + * @description Validator status description (active, jailed, unbonding, etc.) + * @example active + */ + status?: string; + /** + * @description ID of the provider backing this validator + * @example provider-1 + */ + providerId?: string; + /** + * @description Price per share of the validator + * @example 1.0 + */ + pricePerShare?: string; + /** + * @description Subnet ID + * @example 1 + */ + subnetId?: number; + /** + * @description Subnet name + * @example Apex + */ + subnetName?: string; + /** + * @description Market cap of the subnet + * @example 1000000 + */ + marketCap?: string; + /** + * @description Token symbol of the subnet + * @example α + */ + tokenSymbol?: string; + }; + BalanceDto: { + /** + * @description User wallet address that owns this balance + * @example 0x1234... + */ + address: string; + type: components["schemas"]["BalanceType"]; + /** + * @description Balance amount in underlying token + * @example 2.625 + */ + amount: string; + /** + * @description Raw balance amount (full precision) + * @example 2625000000000000000 + */ + amountRaw: string; + /** + * Format: date-time + * @description Date relevant to this balance state + * @example 2025-04-23T08:00:00Z + */ + date?: string | null; + /** + * @description Fee configuration ID (if applicable) + * @example fee-config-1 + */ + feeConfigurationId?: string; + /** @description Pending actions for this balance */ + pendingActions: components["schemas"]["PendingActionDto"][]; + /** @description Token used for balance amounts */ + token: components["schemas"]["TokenDto"]; + /** @description Validator information (if applicable) */ + validator?: components["schemas"]["ValidatorDto"] | null; + /** @description Multiple validators information (when balance is distributed across multiple validators) */ + validators?: components["schemas"]["ValidatorDto"][] | null; + /** + * @description Value of the balance in USD + * @example 2,500.00 + */ + amountUsd?: string | null; + /** + * @description Whether this balance is currently earning rewards + * @example true + */ + isEarning: boolean; + /** + * @description Price range for concentrated liquidity positions in tokens[1]/tokens[0] format (e.g., if tokens[0]=WETH and tokens[1]=USDC, then priceRange represents USDC/WETH) + * @example { + * "min": "2700", + * "max": "3310" + * } + */ + priceRange?: Record; + /** + * @description NFT token ID for liquidity positions (e.g., PancakeSwap V3 position NFT ID) + * @example 12345 + */ + tokenId?: string; + /** + * @description Share balance in human-readable format + * @example 1.5 + */ + shareAmount?: string; + /** + * @description Share balance in full precision (smallest unit) + * @example 1500000000000000000 + */ + shareAmountRaw?: string; + /** @description The share token that shareAmount and shareAmountRaw are denominated in */ + shareToken?: components["schemas"]["TokenDto"]; + }; + YieldBalancesDto: { + /** + * @description Unique identifier of the yield + * @example ethereum-eth-lido-staking + */ + yieldId: string; + /** @description List of balances for this yield */ + balances: components["schemas"]["BalanceDto"][]; + /** @description Balance for the output token */ + outputTokenBalance?: components["schemas"]["BalanceDto"] | null; + /** @description Personalized reward rate breakdown for this balance position */ + rewardRate?: components["schemas"]["RewardRateDto"] | null; + }; + YieldErrorDto: { + /** + * @description Unique identifier of the yield that failed + * @example ethereum-compound-usdc + */ + yieldId: string; + /** + * @description Error message describing what went wrong + * @example Failed to fetch data from blockchain: RPC timeout + */ + error: string; + }; + BalancesResponseDto: { + /** @description Successful yield balance results */ + items: components["schemas"]["YieldBalancesDto"][]; + /** @description Errors encountered while fetching balances */ + errors: components["schemas"]["YieldErrorDto"][]; + }; + /** @enum {string} */ + Networks: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + GetBalancesArgumentsDto: { + /** + * @description Avalanche C-chain address + * @example 0x123... + */ + cAddressBech?: string; + /** + * @description Avalanche P-chain address + * @example P-avax1... + */ + pAddressBech?: string; + /** + * @description Day of month when auto-sweep window starts (used by Solana auto-sweep balance actions) + * @example 20 + */ + autoSweepDayOfMonth?: number; + /** + * @description IANA timezone used to evaluate auto-sweep window day (e.g. Europe/London) + * @example Europe/London + */ + autoSweepTimezone?: string; + }; + BalancesQueryDto: { + /** + * @description The unique identifier of the yield (optional for chain scanning) + * @example ethereum-eth-lido-staking + */ + yieldId?: string; + /** + * @description User wallet address to check balances for + * @example 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + */ + address: string; + /** @example ethereum */ + network: components["schemas"]["Networks"]; + /** @description Arguments for balance queries */ + arguments?: components["schemas"]["GetBalancesArgumentsDto"]; + }; + BalancesRequestDto: { + /** + * @description Array of balance queries (maximum 25 queries per request) + * @example [ + * { + * "yieldId": "ethereum-eth-lido-staking", + * "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + * "network": "ethereum" + * } + * ] + */ + queries: components["schemas"]["BalancesQueryDto"][]; + }; + YieldBalancesRequestDto: { + /** + * @description User wallet address to check balances for + * @example 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + */ + address: string; + /** @description Optional arguments for advanced or protocol-specific balance queries */ + arguments?: components["schemas"]["GetBalancesArgumentsDto"]; + }; + ValidatorQueryDto: { + /** + * @description Offset for pagination + * @default 0 + * @example 0 + */ + offset: number; + /** + * @description Maximum number of items to return + * @default 20 + * @example 20 + */ + limit: number; + /** @description Filter by validator name (case-insensitive, partial match) */ + name?: string; + /** @description Filter by validator address */ + address?: string; + /** @description Filter by provider ID */ + provider?: string; + /** @description Filter by validator status */ + status?: string; + /** @description Filter by preferred flag */ + preferred?: boolean; + }; + TransactionDto: { + /** + * @description Unique transaction identifier + * @example tx_123abc + */ + id: string; + /** + * @description Display title for the transaction + * @example Approve USDC + */ + title: string; + /** + * @description Network this transaction is for + * @example ethereum + * @enum {string} + */ + network: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description Current status of the transaction + * @example PENDING + * @enum {string} + */ + status: + | "NOT_FOUND" + | "CREATED" + | "BLOCKED" + | "WAITING_FOR_SIGNATURE" + | "SIGNED" + | "BROADCASTED" + | "PENDING" + | "CONFIRMED" + | "FAILED" + | "SKIPPED"; + /** + * @description Type of transaction operation + * @example STAKE + * @enum {string} + */ + type: + | "SWAP" + | "DEPOSIT" + | "APPROVAL" + | "STAKE" + | "CLAIM_UNSTAKED" + | "CLAIM_REWARDS" + | "RESTAKE_REWARDS" + | "UNSTAKE" + | "SPLIT" + | "MERGE" + | "LOCK" + | "UNLOCK" + | "SUPPLY" + | "ADD_LIQUIDITY" + | "REMOVE_LIQUIDITY" + | "BRIDGE" + | "VOTE" + | "REVOKE" + | "RESTAKE" + | "REBOND" + | "WITHDRAW" + | "WITHDRAW_ALL" + | "CREATE_ACCOUNT" + | "REVEAL" + | "MIGRATE" + | "DELEGATE" + | "UNDELEGATE" + | "UTXO_P_TO_C_IMPORT" + | "UTXO_C_TO_P_IMPORT" + | "WRAP" + | "UNWRAP" + | "UNFREEZE_LEGACY" + | "UNFREEZE_LEGACY_BANDWIDTH" + | "UNFREEZE_LEGACY_ENERGY" + | "UNFREEZE_BANDWIDTH" + | "UNFREEZE_ENERGY" + | "FREEZE_BANDWIDTH" + | "FREEZE_ENERGY" + | "UNDELEGATE_BANDWIDTH" + | "UNDELEGATE_ENERGY" + | "P2P_NODE_REQUEST" + | "CREATE_EIGENPOD" + | "VERIFY_WITHDRAW_CREDENTIALS" + | "START_CHECKPOINT" + | "VERIFY_CHECKPOINT_PROOFS" + | "QUEUE_WITHDRAWALS" + | "COMPLETE_QUEUED_WITHDRAWALS" + | "LZ_DEPOSIT" + | "LZ_WITHDRAW" + | "LUGANODES_PROVISION" + | "LUGANODES_EXIT_REQUEST" + | "INFSTONES_PROVISION" + | "INFSTONES_EXIT_REQUEST" + | "INFSTONES_CLAIM_REQUEST" + | "BATCH"; + /** + * @description Transaction hash (available after broadcast) + * @example 0x1234567890abcdef... + */ + hash: string | null; + /** + * Format: date-time + * @description When the transaction was created + */ + createdAt: string; + /** + * Format: date-time + * @description When the transaction was broadcasted to the network + */ + broadcastedAt: string | null; + /** @description Signed transaction data (ready for broadcast) */ + signedTransaction: string | null; + /** + * @description The unsigned transaction data to be signed by the wallet + * @example 0x02f87082012a022f2f83018000947a250d5630b4cf539739df2c5dacb4c659f2488d880de0b6b3a764000080c080a0ef0de6c7b46fc75dd6cb86dccc3cfd731c2bdf6f3d736557240c3646c6fe01a6a07cd60b58dfe01847249dfdd7950ba0d045dded5bbe410b07a015a0ed34e5e00d + */ + unsignedTransaction: (string | Record) | null; + /** + * @description Human-readable breakdown of the transaction for display purposes + * @example { + * "method": "stake", + * "inputs": { + * "amount": "1000000000000000000" + * } + * } + */ + annotatedTransaction?: Record | null; + /** @description Detailed transaction data for client-side validation or simulation */ + structuredTransaction?: Record | null; + /** + * @description Zero-based index of the step in the action flow + * @example 0 + */ + stepIndex?: number; + /** + * @description User-friendly description of what this transaction does + * @example Approve USDC for staking + */ + description?: string; + /** @description Error message if the transaction failed */ + error?: string | null; + /** + * @description Estimated gas cost for the transaction + * @example 21000 + */ + gasEstimate?: string; + /** + * @description Link to the blockchain explorer for this transaction + * @example https://etherscan.io/tx/0x1234... + */ + explorerUrl?: string | null; + /** + * @description Whether this transaction is a message rather than a value transfer + * @example false + */ + isMessage?: boolean; + }; + ActionArgumentsDto: { + /** + * @description Amount in human-readable token units, not the smallest denomination. For example, "1.500000" for 1.5 USDC (6 decimals) or "0.01" for 0.01 ETH (18 decimals). Precision up to the token's decimal places is supported. + * @example 1.500000 + */ + amount?: string; + /** + * @description Amounts in human-readable token units, not the smallest denomination. Precision up to the token's decimal places is supported. + * @example [ + * "1.500000", + * "2.000000" + * ] + */ + amounts?: string[]; + /** + * @description Validator address for single validator selection + * @example cosmosvaloper1... + */ + validatorAddress?: string; + /** + * @description Multiple validator addresses + * @example [ + * "cosmosvaloper1...", + * "cosmosvaloper2..." + * ] + */ + validatorAddresses?: string[]; + /** + * @description Provider ID for Ethereum native staking + * @example kiln + */ + providerId?: string; + /** + * @description Duration for Avalanche native staking (in seconds) + * @example 1209600 + */ + duration?: number; + /** + * @description Token for deposits. Use "0x" for native token or provide the token address. For cross-chain deposits, also provide inputTokenNetwork. + * @example 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + */ + inputToken?: string; + /** + * @description Network for the input token. Required for cross-chain deposits when the token is on a different network than the vault. + * @enum {string} + */ + inputTokenNetwork?: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description Token for withdrawals. Use "0x" for native token or provide the token address. For cross-chain withdrawals, also provide outputTokenNetwork. + * @example 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + */ + outputToken?: string; + /** + * @description Network for the output token. Required for cross-chain withdrawals when the destination is on a different network than the vault. + * @enum {string} + */ + outputTokenNetwork?: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description Subnet ID for Bittensor staking + * @example 1 + */ + subnetId?: number; + /** + * @description Tron resource type for Tron staking + * @enum {string} + */ + tronResource?: "BANDWIDTH" | "ENERGY"; + /** + * @description Fee configuration ID for custom fee settings + * @example custom-fee-config-1 + */ + feeConfigurationId?: string; + /** + * @description Cosmos public key for Cosmos staking + * @example cosmospub1... + */ + cosmosPubKey?: string; + /** + * @description Tezos public key for Tezos staking + * @example edpk... + */ + tezosPubKey?: string; + /** + * @description Avalanche C-chain address + * @example 0x123... + */ + cAddressBech?: string; + /** + * @description Avalanche P-chain address + * @example P-avax1... + */ + pAddressBech?: string; + /** + * @description Transaction execution mode + * @example individual + * @enum {string} + */ + executionMode?: "individual" | "batched"; + /** + * @description Transactions should have Ledger wallet API compatibility for hardware wallet users + * @example true + */ + ledgerWalletApiCompatible?: boolean; + /** + * @description Use max amount for ERC4626 withdraw + * @example true + */ + useMaxAmount?: boolean; + /** + * @description Use instant execution for exit (faster but may have fees) + * @example true + */ + useInstantExecution?: boolean; + /** + * @description Skip pre-flight balance and rent checks + * @example false + */ + skipPrechecks?: boolean; + /** + * @description Minimum price bound for concentrated liquidity pools (as decimal string). Must be non-negative (can be 0) and less than rangeMax. + * @example 0.0 + */ + rangeMin?: string; + /** + * @description Maximum price bound for concentrated liquidity pools (as decimal string). Must be positive and greater than rangeMin. + * @example 1.0 + */ + rangeMax?: string; + /** + * @description Percentage of liquidity to exit (0-100). Required for partial exits from liquidity positions. + * @example 50 + */ + percentage?: number; + /** + * @description NFT token ID for concentrated liquidity positions. Required for exiting specific positions. + * @example 12345 + */ + tokenId?: string; + }; + ActionDto: { + /** + * @description Unique action identifier + * @example action_123abc + */ + id: string; + /** + * @description High-level action intent + * @example manage + * @enum {string} + */ + intent: "enter" | "manage" | "exit"; + /** + * @description Specific action type + * @example CLAIM_REWARDS + * @enum {string} + */ + type: + | "STAKE" + | "UNSTAKE" + | "CLAIM_REWARDS" + | "AUTO_SWEEP_UNSTAKE_REWARDS" + | "AUTO_SWEEP_WITHDRAW_REWARDS" + | "RESTAKE_REWARDS" + | "WITHDRAW" + | "WITHDRAW_ALL" + | "RESTAKE" + | "CLAIM_UNSTAKED" + | "UNLOCK_LOCKED" + | "STAKE_LOCKED" + | "VOTE" + | "REVOKE" + | "VOTE_LOCKED" + | "REVOTE" + | "REBOND" + | "MIGRATE" + | "VERIFY_WITHDRAW_CREDENTIALS" + | "DELEGATE"; + /** + * @description Yield ID this action belongs to + * @example ethereum-eth-lido-staking + */ + yieldId: string; + /** + * @description User wallet address + * @example 0x1234... + */ + address: string; + /** + * @description Amount involved in the action, in human-readable token units (not the smallest denomination). + * @example 1.0 + */ + amount: string | null; + /** + * @description Raw smallest-denomination amount (full precision) + * @example 1000000000000000000 + */ + amountRaw: string | null; + /** + * @description USD value of the amount + * @example 1500.50 + */ + amountUsd: string | null; + /** @description Array of transactions for this action */ + transactions: components["schemas"]["TransactionDto"][]; + /** + * @description Transaction execution pattern - synchronous (submit one by one, wait for each), asynchronous (submit all at once), or batch (single transaction with multiple operations) + * @example synchronous + * @enum {string} + */ + executionPattern: "synchronous" | "asynchronous" | "batch"; + /** @description Raw arguments exactly as submitted by the user for this action */ + rawArguments: components["schemas"]["ActionArgumentsDto"] | null; + /** + * Format: date-time + * @description When the action was created + */ + createdAt: string; + /** + * Format: date-time + * @description When the action was completed + */ + completedAt: string | null; + /** + * @description Current status of the action + * @enum {string} + */ + status: + | "CANCELED" + | "CREATED" + | "WAITING_FOR_NEXT" + | "PROCESSING" + | "FAILED" + | "SUCCESS" + | "STALE"; + }; + PaginatedResponseDto: { + /** + * @description Total number of items available + * @example 100 + */ + total: number; + /** + * @description Offset of the current page + * @example 0 + */ + offset: number; + /** + * @description Limit of the current page + * @example 20 + */ + limit: number; + }; + YieldQueryDto: { + /** + * @description Offset for pagination + * @default 0 + * @example 0 + */ + offset: number; + /** + * @description Maximum number of items to return + * @default 20 + * @example 20 + */ + limit: number; + /** + * @description Filter by network + * @enum {string} + */ + network?: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description Filter by EVM chain ID (Ethereum: 1, Polygon: 137, etc) + * @example 1 + */ + chainId?: string; + /** @description Filter by multiple networks (comma separated) */ + networks?: string; + /** @example optimism-usdt-aave-v3-lending */ + yieldId?: string; + /** + * @example [ + * "optimism-usdt-aave-v3-lending" + * ] + */ + yieldIds?: string[]; + /** + * @description Filter by yield type + * @enum {string} + */ + type?: + | "staking" + | "restaking" + | "lending" + | "vault" + | "fixed_yield" + | "real_world_asset" + | "concentrated_liquidity_pool" + | "liquidity_pool"; + /** @description Filter by multiple yield types (comma separated) */ + types?: ( + | "staking" + | "restaking" + | "lending" + | "vault" + | "fixed_yield" + | "real_world_asset" + | "concentrated_liquidity_pool" + | "liquidity_pool" + )[]; + /** @description Filter by cooldown period */ + hasCooldownPeriod?: boolean; + /** @description Filter by warmup period */ + hasWarmupPeriod?: boolean; + /** @description Filter by token symbol or address */ + token?: string; + /** @description Filter by input token symbol or address */ + inputToken?: string; + /** @description Filter by multiple input token symbol or address (comma separated) */ + inputTokens?: string[]; + /** @description Filter by provider ID */ + provider?: string; + /** @description Filter by multiple provider IDs (comma separated) */ + providers?: string[]; + /** @description Search by yield name */ + search?: string; + /** + * @description Sort by yield status + * @enum {string} + */ + sort?: + | "statusEnterAsc" + | "statusEnterDesc" + | "statusExitAsc" + | "statusExitDesc"; + }; + PaginationQueryDto: { + /** + * @description Offset for pagination + * @default 0 + * @example 0 + */ + offset: number; + /** + * @description Maximum number of items to return + * @default 20 + * @example 20 + */ + limit: number; + }; + RiskParameterDto: { + id: string; + category: string; + item: string; + isDynamic: boolean; + value?: Record; + network?: components["schemas"]["Networks"]; + asset?: Record; + protocol?: Record; + integrationId?: Record; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + }; + BalanceHistorySnapshotDto: { + /** + * @description Timestamp of this snapshot (ISO 8601) + * @example 2025-07-12T00:00:00.000Z + */ + timestamp: string; + /** + * @description Block number closest to this snapshot + * @example 20540000 + */ + blockNumber: number; + /** + * @description Unique identifier of the yield + * @example ethereum-eth-lido-staking + */ + yieldId: string; + /** @description Balance entries at this point in time */ + balances: components["schemas"]["BalanceDto"][]; + }; + RewardEventDto: { + /** + * @description Timestamp of this reward event (ISO 8601) + * @example 2025-07-12T00:00:00.000Z + */ + timestamp: string; + /** + * @description Block number when the reward was earned + * @example 20540000 + */ + blockNumber: number; + /** + * @description Unique identifier of the yield + * @example ethereum-usdc-morpho-vault + */ + yieldId: string; + /** @description Token metadata for the reward */ + token: components["schemas"]["TokenDto"]; + /** + * @description Human-readable reward amount + * @example 1.4 + */ + amount: string; + /** + * @description Raw reward amount in base units (wei) + * @example 1400000 + */ + amountRaw: string; + /** + * @description Source of the reward derived from yield type + * @example vault_yield + */ + yieldSource: string; + /** + * @description Transaction hash where the reward was earned + * @example 0xabc123... + */ + transactionHash?: string | null; + }; + RewardRateSnapshotDto: { + /** + * @description Timestamp of this snapshot (ISO 8601) + * @example 2025-07-10T00:00:00.000Z + */ + timestamp: string; + /** + * @description Reward rate as a decimal string + * @example 0.0312 + */ + rewardRate: string; + }; + RewardRateHistoryResponseDto: { + /** + * @description Unique identifier of the yield + * @example ethereum-eth-lido-staking + */ + yieldId: string; + /** + * @description Sampling interval used for this response + * @example day + * @enum {string} + */ + interval: "day" | "week" | "month"; + /** + * @description Start of the returned date range (ISO 8601) + * @example 2025-06-01T00:00:00.000Z + */ + from: string; + /** + * @description End of the returned date range (ISO 8601) + * @example 2025-07-10T00:00:00.000Z + */ + to: string; + /** @description Chronological reward rate snapshots (most recent first) */ + series: components["schemas"]["RewardRateSnapshotDto"][]; + }; + TvlSnapshotDto: { + /** + * @description Timestamp of this snapshot (ISO 8601) + * @example 2025-07-11T00:00:00.000Z + */ + timestamp: string; + /** + * @description Total value locked in token units (human-readable) + * @example 512340000.12 + */ + tvl: string; + /** + * @description Total value locked in smallest token unit (wei) + * @example 512340000120000 + */ + tvlRaw: string; + }; + TvlHistoryResponseDto: { + /** + * @description Unique identifier of the yield + * @example ethereum-usdc-aave-v3 + */ + yieldId: string; + /** + * @description Sampling interval used for this response + * @example day + * @enum {string} + */ + interval: "day" | "week" | "month"; + /** + * @description Start of the returned date range (ISO 8601) + * @example 2025-06-12T00:00:00.000Z + */ + from: string; + /** + * @description End of the returned date range (ISO 8601) + * @example 2025-07-12T00:00:00.000Z + */ + to: string; + /** @description Chronological TVL snapshots (most recent first) */ + series: components["schemas"]["TvlSnapshotDto"][]; + }; + CreateActionDto: { + /** + * @description Yield ID to perform the action on + * @example ethereum-eth-lido-staking + */ + yieldId: string; + /** + * @description User wallet address + * @example 0x1234... + */ + address: string; + /** @description Arguments for the action */ + arguments?: components["schemas"]["ActionArgumentsDto"]; + }; + CreateManageActionDto: { + /** + * @description Yield ID to perform the action on + * @example ethereum-eth-lido-staking + */ + yieldId: string; + /** + * @description User wallet address + * @example 0x1234... + */ + address: string; + /** @description Arguments for the action */ + arguments?: components["schemas"]["ActionArgumentsDto"]; + /** + * @description Pending action type (required for manage actions) + * @example CLAIM_REWARDS + * @enum {string} + */ + action: + | "STAKE" + | "UNSTAKE" + | "CLAIM_REWARDS" + | "AUTO_SWEEP_UNSTAKE_REWARDS" + | "AUTO_SWEEP_WITHDRAW_REWARDS" + | "RESTAKE_REWARDS" + | "WITHDRAW" + | "WITHDRAW_ALL" + | "RESTAKE" + | "CLAIM_UNSTAKED" + | "UNLOCK_LOCKED" + | "STAKE_LOCKED" + | "VOTE" + | "REVOKE" + | "VOTE_LOCKED" + | "REVOTE" + | "REBOND" + | "MIGRATE" + | "VERIFY_WITHDRAW_CREDENTIALS" + | "DELEGATE"; + /** + * @description Server-generated passthrough from the balances endpoint (required for manage actions) + * @example eyJhZGRyZXNzZXMiOnsiYWRkcmVzcyI6ImNvc21vczF5ZXk... + */ + passthrough: string; + }; + ActionsQueryDto: { + /** + * @description Offset for pagination + * @default 0 + * @example 0 + */ + offset: number; + /** + * @description Maximum number of items to return + * @default 20 + * @example 20 + */ + limit: number; + /** + * @description User wallet address to filter actions for + * @example 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + */ + address: string; + /** + * @description Filter by action status + * @enum {string} + */ + status?: + | "CANCELED" + | "CREATED" + | "WAITING_FOR_NEXT" + | "PROCESSING" + | "FAILED" + | "SUCCESS" + | "STALE"; + /** + * @description Filter by action intent + * @enum {string} + */ + intent?: "enter" | "manage" | "exit"; + /** + * @description Filter by action type + * @enum {string} + */ + type?: + | "STAKE" + | "UNSTAKE" + | "CLAIM_REWARDS" + | "AUTO_SWEEP_UNSTAKE_REWARDS" + | "AUTO_SWEEP_WITHDRAW_REWARDS" + | "RESTAKE_REWARDS" + | "WITHDRAW" + | "WITHDRAW_ALL" + | "RESTAKE" + | "CLAIM_UNSTAKED" + | "UNLOCK_LOCKED" + | "STAKE_LOCKED" + | "VOTE" + | "REVOKE" + | "VOTE_LOCKED" + | "REVOTE" + | "REBOND" + | "MIGRATE" + | "VERIFY_WITHDRAW_CREDENTIALS" + | "DELEGATE"; + /** + * @description Filter by yield ID + * @example ethereum-eth-lido-staking + */ + yieldId?: string; + }; + SubmitHashDto: { + /** + * @description Transaction hash from the blockchain + * @example 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef + */ + hash: string; + }; + SubmitTransactionDto: { + /** + * @description Encoded signed transaction to submit to the blockchain + * @example 0aba010aa0010a232f636f736d6f732e7374616b696e672e763162657461312e4d736744656c656761746512790a2a696e6a316a61366664646e6e33727272677137646d6a757a6b71363279376d68346675346b6e656d37791231696e6a76616c6f7065723167346436646d766e706737773779756779366b706c6e6470376a70666d66336b7274736368701a180a03696e6a121131303030303030303030303030303030301215766961205374616b654b6974204349442d31303039129e010a7e0a740a2d2f696e6a6563746976652e63727970746f2e763162657461312e657468736563703235366b312e5075624b657912430a41042aec99dce37ea3d8f11b44da62bce0e885f0ba5b309382954babec76eb138cb0bb84f4f24b9f63143f2ce66923b2dd3ee55475e680a7b992b9cbc17941f6486312040a0208011802121c0a160a03696e6a120f31383732303030303030303030303010d0b4471a0b696e6a6563746976652d312092c35b + */ + signedTransaction: string; + }; + NetworkDto: { + /** + * @description The network identifier + * @example ethereum + * @enum {string} + */ + id: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description Human-readable display name of the network + * @example Ethereum + */ + name: string; + /** + * @description The category of the network + * @example evm + * @enum {string} + */ + category: "evm" | "cosmos" | "substrate" | "misc"; + /** + * @description Logo URI for the network + * @example https://assets.stakek.it/networks/ethereum.svg + */ + logoURI: string; + }; + ProviderDto: { + /** + * @description Provider name + * @example Morpho + */ + name: string; + /** + * @description Provider ID + * @example morpho + */ + id: string; + /** + * @description Provider logo URI + * @example https://morpho.xyz/logo.png + */ + logoURI: string; + /** + * @description Short description of the provider + * @example A peer-to-peer DeFi lending protocol + */ + description: string; + /** + * @description Provider website + * @example https://morpho.xyz + */ + website: string; + /** + * @description Total TVL across the entire provider in USD + * @example 10,200,000 + */ + tvlUsd: Record | null; + /** + * @description Type of provider (protocol or validator provider) + * @example protocol + * @enum {string} + */ + type: "protocol" | "validator_provider"; + /** @description Optional social/media references or audit links */ + references?: string[] | null; + }; + /** + * @description The health status of the service + * @enum {string} + */ + HealthStatus: "OK" | "FAIL"; + HealthStatusDto: { + /** @example OK */ + status: components["schemas"]["HealthStatus"]; + /** + * Format: date-time + * @description Timestamp when the health check was performed + * @example 2024-01-15T10:30:00.000Z + */ + timestamp: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + YieldsController_getYields: { + parameters: { + query?: { + /** + * @description Offset for pagination + * @example 0 + */ + offset?: number; + /** + * @description Number of items per page + * @example 20 + */ + limit?: number; + /** @description Filter by network */ + network?: + | "ethereum" + | "ethereum-goerli" + | "ethereum-holesky" + | "ethereum-sepolia" + | "ethereum-hoodi" + | "arbitrum" + | "base" + | "base-sepolia" + | "gnosis" + | "optimism" + | "polygon" + | "polygon-amoy" + | "starknet" + | "zksync" + | "linea" + | "unichain" + | "monad-testnet" + | "monad" + | "avalanche-c" + | "avalanche-c-atomic" + | "avalanche-p" + | "binance" + | "celo" + | "fantom" + | "harmony" + | "moonriver" + | "okc" + | "viction" + | "core" + | "sonic" + | "plasma" + | "katana" + | "hyperevm" + | "agoric" + | "akash" + | "axelar" + | "band-protocol" + | "bitsong" + | "canto" + | "chihuahua" + | "comdex" + | "coreum" + | "cosmos" + | "crescent" + | "cronos" + | "cudos" + | "desmos" + | "dydx" + | "evmos" + | "fetch-ai" + | "gravity-bridge" + | "injective" + | "irisnet" + | "juno" + | "kava" + | "ki-network" + | "mars-protocol" + | "nym" + | "okex-chain" + | "onomy" + | "osmosis" + | "persistence" + | "quicksilver" + | "regen" + | "secret" + | "sentinel" + | "sommelier" + | "stafi" + | "stargaze" + | "stride" + | "teritori" + | "tgrade" + | "umee" + | "sei" + | "mantra" + | "celestia" + | "saga" + | "zetachain" + | "dymension" + | "humansai" + | "neutron" + | "polkadot" + | "kusama" + | "westend" + | "bittensor" + | "aptos" + | "binancebeacon" + | "cardano" + | "near" + | "solana" + | "solana-devnet" + | "stellar" + | "stellar-testnet" + | "sui" + | "tezos" + | "tron" + | "ton" + | "ton-testnet" + | "hyperliquid"; + /** + * @description Filter by EVM chain ID (Ethereum: 1, Polygon: 137) + * @example 1 + */ + chainId?: string; + /** @description Filter by multiple networks (comma separated) */ + networks?: string; + /** @example optimism-usdt-aave-v3-lending */ + yieldId?: string; + /** + * @example [ + * "optimism-usdt-aave-v3-lending" + * ] + */ + yieldIds?: string[]; + /** @description Filter by yield type */ + type?: + | "staking" + | "restaking" + | "lending" + | "vault" + | "fixed_yield" + | "real_world_asset" + | "concentrated_liquidity_pool" + | "liquidity_pool"; + /** @description Filter by multiple yield types (comma separated) */ + types?: ( + | "staking" + | "restaking" + | "lending" + | "vault" + | "fixed_yield" + | "real_world_asset" + | "concentrated_liquidity_pool" + | "liquidity_pool" + )[]; + /** + * @description Filter by cooldown period + * @example true + */ + hasCooldownPeriod?: boolean; + /** + * @description Filter by warmup period + * @example true + */ + hasWarmupPeriod?: boolean; + /** @description Filter by token symbol or address */ + token?: string; + /** @description Filter by input token symbol or address */ + inputToken?: string; + /** @description Filter by multiple input token symbol or address (comma separated) */ + inputTokens?: string[]; + /** @description Filter by provider ID */ + provider?: string; + /** @description Filter by multiple provider IDs (comma separated) */ + providers?: string[]; + /** @description Search by yield name */ + search?: string; + /** @description Sort by yield status */ + sort?: + | "statusEnterAsc" + | "statusEnterDesc" + | "statusExitAsc" + | "statusExitDesc"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a paginated list of yield opportunities */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseDto"] & { + items?: components["schemas"]["YieldDto"][]; + }; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getAggregateBalances: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Request containing an array of balance queries. Each query contains: yieldId (optional), address (required), network (required), and arguments (optional). When yieldId is omitted, all yields for that network will be scanned. You can mix chain scans with specific yield queries - duplicates are automatically deduplicated with specific queries taking precedence. */ + requestBody: { + content: { + "application/json": components["schemas"]["BalancesRequestDto"]; + }; + }; + responses: { + /** @description Returns balances grouped by yield with detailed error information for failed yields. Only yields with non-zero balances are included in the response. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BalancesResponseDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getYield: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description The unique identifier of the yield + * @example ethereum-eth-lido-staking + */ + yieldId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns yield metadata including network, APR, TVL, and token information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["YieldDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getYieldRisk: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description The unique identifier of the yield + * @example ethereum-eth-lido-staking + */ + yieldId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Risk metadata retrieved successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RiskParameterDto"][]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getBalanceHistory: { + parameters: { + query: { + /** + * @description Wallet address to fetch history for + * @example 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + */ + address: string; + /** + * @description Start of time range (ISO 8601) + * @example 2025-01-01T00:00:00Z + */ + from?: string; + /** + * @description End of time range (ISO 8601). Defaults to now. + * @example 2025-07-12T00:00:00Z + */ + to?: string; + /** + * @description Block number for a point-in-time snapshot. When provided, from/to/interval are ignored. + * @example 20540000 + */ + blockNumber?: number; + /** @description Sampling resolution for the time series */ + interval?: "block" | "hour" | "day" | "week"; + /** @description Sort order by timestamp. Defaults to most recent first (desc). */ + sort?: "asc" | "desc"; + /** + * @description Maximum number of items to return (default 30, max 100) + * @example 30 + */ + limit?: number; + /** + * @description Pagination offset + * @example 0 + */ + offset?: number; + }; + header?: never; + path: { + /** + * @description The unique identifier of the yield + * @example ethereum-eth-lido-staking + */ + yieldId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a paginated time series of balance snapshots */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseDto"] & { + items?: components["schemas"]["BalanceHistorySnapshotDto"][]; + }; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getYieldBalances: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description The unique identifier of the yield opportunity + * @example ethereum-eth-lido-staking + */ + yieldId: string; + }; + cookie?: never; + }; + /** @description Balance request with address and optional arguments for advanced balance queries */ + requestBody: { + content: { + "application/json": components["schemas"]["YieldBalancesRequestDto"]; + }; + }; + responses: { + /** @description Returns balance information including different balance types (active, entering, exiting, withdrawable, claimable, locked) with amounts and available actions */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["YieldBalancesDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getYieldRewards: { + parameters: { + query: { + /** + * @description Wallet address to fetch rewards for + * @example 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + */ + address: string; + /** + * @description Start of time range (ISO 8601) + * @example 2025-01-01T00:00:00Z + */ + from?: string; + /** + * @description End of time range (ISO 8601) + * @example 2025-07-12T00:00:00Z + */ + to?: string; + /** @description Sort order by timestamp (default: desc) */ + sort?: "asc" | "desc"; + /** + * @description Maximum number of items to return (default: 100, max: 100) + * @example 100 + */ + limit?: number; + /** + * @description Pagination offset (default: 0) + * @example 0 + */ + offset?: number; + }; + header?: never; + path: { + /** + * @description The unique identifier of the yield + * @example ethereum-usdc-morpho-vault + */ + yieldId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated reward events */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Reward history not available for this yield (not indexed) */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getYieldRewardRateHistory: { + parameters: { + query?: { + /** + * @description Start of time range (ISO 8601). Overrides period when provided. + * @example 2025-01-01T00:00:00Z + */ + from?: string; + /** + * @description End of time range (ISO 8601). Defaults to now. + * @example 2025-07-10T00:00:00Z + */ + to?: string; + /** @description Predefined time window. Ignored when from/to are provided. Default: 30d. */ + period?: "1d" | "7d" | "30d" | "90d" | "1y" | "all"; + /** @description Sampling resolution (day/week/month). Default: day. */ + interval?: "day" | "week" | "month"; + /** @description Maximum number of data points to return (default 100, max 365) */ + limit?: number; + /** @description Pagination offset */ + offset?: number; + }; + header?: never; + path: { + /** + * @description The unique identifier of the yield + * @example ethereum-eth-lido-staking + */ + yieldId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a time series of reward rate snapshots */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RewardRateHistoryResponseDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getYieldTvlHistory: { + parameters: { + query?: { + /** + * @description Start of time range (ISO 8601). Overrides period when provided. + * @example 2025-01-01T00:00:00Z + */ + from?: string; + /** + * @description End of time range (ISO 8601). Defaults to now. + * @example 2025-07-10T00:00:00Z + */ + to?: string; + /** @description Predefined time window. Ignored when from/to are provided. Default: 30d. */ + period?: "1d" | "7d" | "30d" | "90d" | "1y" | "all"; + /** @description Sampling resolution (day/week/month). Default: day. */ + interval?: "day" | "week" | "month"; + /** @description Maximum number of data points to return (default 100, max 365) */ + limit?: number; + /** @description Pagination offset */ + offset?: number; + }; + header?: never; + path: { + /** + * @description The unique identifier of the yield + * @example ethereum-usdc-aave-v3-lending + */ + yieldId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a time series of TVL snapshots in token units */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TvlHistoryResponseDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + YieldsController_getYieldValidators: { + parameters: { + query?: { + /** + * @description Offset for pagination + * @example 0 + */ + offset?: number; + /** + * @description Number of items per page + * @example 20 + */ + limit?: number; + /** @description Filter by validator name (case-insensitive, partial match) */ + name?: string; + /** @description Filter by validator address */ + address?: string; + /** @description Filter by provider ID */ + provider?: string; + /** @description Filter by validator status */ + status?: string; + /** @description Filter by preferred flag */ + preferred?: boolean; + }; + header?: never; + path: { + /** + * @description The unique identifier of the yield + * @example solana-sol-native-multivalidator-staking + */ + yieldId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns paginated list of available validators with detailed information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseDto"] & { + items?: components["schemas"]["ValidatorDto"][]; + }; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + ActionsController_getActions: { + parameters: { + query: { + /** + * @description Offset for pagination + * @example 0 + */ + offset?: number; + /** + * @description Maximum number of items to return + * @example 20 + */ + limit?: number; + /** + * @description User wallet address to get actions for + * @example 0x742d35Cc6634C0532925a3b844Bc454e4438f44e + */ + address: string; + /** @description Filter actions by status */ + status?: "pending" | "completed" | "failed"; + /** @description Filter actions by intent */ + intent?: "enter" | "exit" | "manage"; + /** @description Filter by action type */ + type?: + | "STAKE" + | "UNSTAKE" + | "CLAIM_REWARDS" + | "AUTO_SWEEP_UNSTAKE_REWARDS" + | "AUTO_SWEEP_WITHDRAW_REWARDS" + | "RESTAKE_REWARDS" + | "WITHDRAW" + | "WITHDRAW_ALL" + | "RESTAKE" + | "CLAIM_UNSTAKED" + | "UNLOCK_LOCKED" + | "STAKE_LOCKED" + | "VOTE" + | "REVOKE" + | "VOTE_LOCKED" + | "REVOTE" + | "REBOND" + | "MIGRATE" + | "VERIFY_WITHDRAW_CREDENTIALS" + | "DELEGATE"; + /** + * @description Filter actions by specific yield + * @example ethereum-eth-lido-staking + */ + yieldId?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a paginated list of user actions */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseDto"] & { + items?: components["schemas"]["ActionDto"][]; + }; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + ActionsController_getAction: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description The unique identifier of the action + * @example action_123abc + */ + actionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Action details retrieved successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ActionDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Action not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + ActionsController_enterYield: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateActionDto"]; + }; + }; + responses: { + /** @description Returns action with array of transactions to execute */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ActionDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Access denied due to geolocation restrictions */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Access denied from US (US-CA) */ + message?: string; + /** @example Forbidden */ + error?: string; + /** @example 403 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + ActionsController_exitYield: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateActionDto"]; + }; + }; + responses: { + /** @description Returns action with array of transactions to execute */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ActionDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Access denied due to geolocation restrictions */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Access denied from US (US-CA) */ + message?: string; + /** @example Forbidden */ + error?: string; + /** @example 403 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + ActionsController_manageYield: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateManageActionDto"]; + }; + }; + responses: { + /** @description Returns action with array of transactions to execute */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ActionDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Access denied due to geolocation restrictions */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Access denied from US (US-CA) */ + message?: string; + /** @example Forbidden */ + error?: string; + /** @example 403 */ + statusCode?: number; + }; + }; + }; + /** @description Yield not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + TransactionsController_submitTransactionHash: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description The unique identifier of the transaction + * @example 21f5fd15-cf80-4b9a-804f-062c40dc3740 + */ + transactionId: unknown; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SubmitHashDto"]; + }; + }; + responses: { + /** @description Transaction successfully updated with hash */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TransactionDto"]; + }; + }; + /** @description Invalid transaction hash format */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Transaction not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + TransactionsController_submitTransaction: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description The unique identifier of the transaction + * @example 21f5fd15-cf80-4b9a-804f-062c40dc3740 + */ + transactionId: unknown; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SubmitTransactionDto"]; + }; + }; + responses: { + /** @description Transaction successfully submitted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TransactionDto"]; + }; + }; + /** @description Invalid transaction format */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Transaction not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + TransactionsController_getTransaction: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description The unique identifier of the transaction + * @example 21f5fd15-cf80-4b9a-804f-062c40dc3740 + */ + transactionId: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Transaction details retrieved successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TransactionDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Transaction not found with the specified ID */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + NetworksController_getNetworks: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of all available networks */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkDto"][]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + ProvidersController_getProviders: { + parameters: { + query?: { + /** + * @description Offset for pagination + * @example 0 + */ + offset?: number; + /** + * @description Number of items per page + * @example 20 + */ + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of providers */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseDto"] & { + items?: components["schemas"]["ProviderDto"][]; + }; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + ProvidersController_getProvider: { + parameters: { + query?: never; + header?: never; + path: { + providerId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Provider details */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProviderDto"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Validation failed */ + message?: string; + /** @example Bad Request */ + error?: string; + /** @example 400 */ + statusCode?: number; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid API key */ + message?: string; + /** @example Unauthorized */ + error?: string; + /** @example 401 */ + statusCode?: number; + }; + }; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + /** @description Request limit per window */ + "x-ratelimit-limit"?: string; + /** @description Remaining requests (will be 0) */ + "x-ratelimit-remaining"?: string; + /** @description Unix timestamp when window resets */ + "x-ratelimit-reset"?: string; + /** @description Seconds to wait before retrying */ + "retry-after"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Rate limit exceeded */ + message?: string; + /** @example Too Many Requests */ + error?: string; + /** @example 429 */ + statusCode?: number; + /** @example 30 */ + retryAfter?: number; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Internal server error */ + message?: string; + /** @example Internal Server Error */ + error?: string; + /** @example 500 */ + statusCode?: number; + }; + }; + }; + }; + }; + HealthController_health: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Health check status with timestamp */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthStatusDto"]; + }; + }; + }; + }; +} diff --git a/packages/widget/src/utils/formatters.ts b/packages/widget/src/utils/formatters.ts index 82d8dc56..81b4c5dc 100644 --- a/packages/widget/src/utils/formatters.ts +++ b/packages/widget/src/utils/formatters.ts @@ -3,6 +3,7 @@ import type BigNumber from "bignumber.js"; import { Maybe } from "purify-ts"; import { getTokenPriceInUSD } from "../domain"; import { Prices } from "../domain/types/price"; +import type { YieldTokenDto } from "../providers/yield-api-client-provider/types"; import { APToPercentage, defaultFormattedNumber, formatNumber } from "."; export const formatCountryCode = ({ @@ -79,7 +80,7 @@ export const getFeesInUSD = ({ token, }: { amount: Maybe; - token: Maybe; + token: Maybe; prices: Maybe; }) => Maybe.fromRecord({ token, amount }) diff --git a/packages/widget/src/worker.ts b/packages/widget/src/worker.ts index e4608e7e..fb4b7385 100644 --- a/packages/widget/src/worker.ts +++ b/packages/widget/src/worker.ts @@ -1,190 +1,281 @@ import { HttpResponse, http } from "msw"; import { setupWorker } from "msw/browser"; -// const validAddressAndNetwork = { -// address: "akash12z0hpqxj3txaf85zlla7zqffp7n9sl8wc3hlzh", -// network: "akash", -// }; +const yieldId = "ethereum-matic-native-staking"; + +const maticToken = { + address: "0x0000000000000000000000000000000000001010", + symbol: "POL", + name: "Polygon Ecosystem Token", + decimals: 18, + network: "ethereum", + coinGeckoId: "matic-network", + logoURI: "https://assets.stakek.it/tokens/matic.svg", + isPoints: false, +}; + +const morphoToken = { + address: "0x58d97b57bb95320f9a05dc918aef65434969c2b3", + symbol: "MORPHO", + name: "Morpho Token", + decimals: 18, + network: "ethereum", + isPoints: false, +}; + +const campaignToken = { + address: "0x58d97b57bb95320f9a05dc918aef65434969c2b2", + symbol: "U", + name: "United Stables", + decimals: 18, + network: "ethereum", + isPoints: false, +}; + +const discoveryRewardRate = { + total: 0.045507546653006034, + rateType: "APY", + components: [ + { + rate: 0.0028386677110199426, + rateType: "APR", + token: morphoToken, + yieldSource: "protocol_incentive", + description: "MORPHO rewards", + }, + { + rate: 0.002, + rateType: "APR", + token: campaignToken, + yieldSource: "campaign_incentive", + description: "U rewards", + }, + { + rate: 0.042668878941986094, + rateType: "APY", + token: campaignToken, + yieldSource: "staking", + description: "Native staking APY", + }, + ], +}; + +const personalizedRewardRate = { + total: 0.04530754665300604, + rateType: "APY", + components: [ + { + rate: 0.0028386677110199426, + rateType: "APR", + token: morphoToken, + yieldSource: "protocol_incentive", + description: "MORPHO rewards", + }, + { + rate: 0.0018, + rateType: "APR", + token: campaignToken, + yieldSource: "campaign_incentive", + description: "U rewards", + }, + { + rate: 0.042668878941986094, + rateType: "APY", + token: campaignToken, + yieldSource: "staking", + description: "Native staking APY", + }, + ], +}; + +const legacyYieldDto = { + id: yieldId, + token: maticToken, + tokens: [maticToken], + rewardRate: discoveryRewardRate.total, + rewardType: "apy", + apy: discoveryRewardRate.total, + feeConfigurations: [], + args: { + enter: { + addresses: { + address: { + required: true, + network: "ethereum", + }, + }, + args: { + amount: { + required: true, + minimum: 0, + }, + }, + }, + exit: { + addresses: { + address: { + required: true, + network: "ethereum", + }, + }, + args: { + amount: { + required: true, + minimum: 0, + }, + }, + }, + }, + metadata: { + name: "Trust POL Staking", + description: "Local mock for campaign APY QA", + documentation: "https://trustwallet.com", + logoURI: "https://assets.stakek.it/tokens/matic.svg", + type: "staking", + token: maticToken, + tokens: [maticToken], + rewardTokens: [campaignToken, morphoToken], + rewardClaiming: "auto", + rewardSchedule: "day", + gasFeeToken: maticToken, + fee: { + enabled: false, + depositFee: false, + managementFee: false, + performanceFee: false, + }, + provider: { + id: "benqi", + name: "Trust", + description: "", + externalLink: "https://trustwallet.com", + logoURI: "https://assets.stakek.it/providers/benqi.svg", + }, + supportsLedgerWalletApi: true, + supportsMultipleValidators: false, + }, + status: { + enter: true, + exit: true, + }, + validators: [], +}; + +const yieldApiYieldDto = { + id: yieldId, + token: maticToken, + tokens: [maticToken], + inputTokens: [maticToken], + outputToken: maticToken, + network: "ethereum", + chainId: "1", + providerId: "benqi", + rewardRate: discoveryRewardRate, + metadata: { + name: "Trust POL Staking", + description: "Local mock for campaign APY QA", + documentation: "https://trustwallet.com", + logoURI: "https://assets.stakek.it/tokens/matic.svg", + }, + mechanics: { + type: "staking", + gasFeeToken: maticToken, + rewardClaiming: "auto", + rewardSchedule: "day", + supportsLedgerWalletApi: true, + requiresValidatorSelection: false, + arguments: { + enter: { + fields: [ + { + name: "amount", + type: "string", + label: "Amount", + required: true, + minimum: "0", + }, + ], + }, + exit: { + fields: [ + { + name: "amount", + type: "string", + label: "Amount", + required: true, + minimum: "0", + }, + ], + }, + }, + }, + status: { + enter: true, + exit: true, + }, +}; export const worker = setupWorker( + http.get("*/v1/yields/ethereum-matic-native-staking", async ({ request }) => { + const url = new URL(request.url); + + return HttpResponse.json( + url.searchParams.has("ledgerWalletAPICompatible") + ? legacyYieldDto + : yieldApiYieldDto + ); + }), + http.post("*/v1/yields/balances", async () => { + return HttpResponse.json({ + items: [ + { + yieldId, + balances: [ + { + address: "0x15775b23340c0f50e0428d674478b0e9d3d0a759", + amount: "1000251.8279906842", + amountRaw: "10002518279906842", + type: "active", + token: maticToken, + pendingActions: [], + amountUsd: "1000355.009527", + isEarning: true, + }, + ], + rewardRate: personalizedRewardRate, + }, + ], + errors: [], + }); + }), + http.post("*/v1/balances", async () => { + return HttpResponse.json({ + items: [ + { + yieldId, + balances: [ + { + address: "0x15775b23340c0f50e0428d674478b0e9d3d0a759", + amount: "1000251.8279906842", + amountRaw: "10002518279906842", + type: "active", + token: maticToken, + pendingActions: [], + amountUsd: "1000355.009527", + isEarning: true, + }, + ], + rewardRate: personalizedRewardRate, + }, + ], + errors: [], + }); + }), http.post("*/v1/actions/enter/estimate-gas", async () => { return HttpResponse.json({ amount: "0.1", - token: { - network: "polygon", - coinGeckoId: "matic-network", - name: "Polygon", - decimals: 18, - symbol: "MATIC", - logoURI: "https://assets.stakek.it/tokens/matic.svg", - }, + token: maticToken, gasLimit: "", }); }) - // http.get("*/v1/yields/celo-celo-native-staking", async () => { - // await delay(); - - // return HttpResponse.json({ - // id: "celo-celo-native-staking", - // token: { - // name: "Celo", - // symbol: "CELO", - // decimals: 18, - // address: "0x471EcE3750Da237f93B8E339c536989b8978a438", - // network: "celo", - // coinGeckoId: "celo", - // logoURI: "https://assets.stakek.it/tokens/celo.svg", - // }, - // tokens: [ - // { - // name: "Celo", - // symbol: "CELO", - // decimals: 18, - // address: "0x471EcE3750Da237f93B8E339c536989b8978a438", - // network: "celo", - // coinGeckoId: "celo", - // logoURI: "https://assets.stakek.it/tokens/celo.svg", - // }, - // ], - // args: { - // enter: { - // addresses: { - // address: { - // required: true, - // network: "celo", - // }, - // }, - // args: { - // amount: { - // required: true, - // minimum: 0, - // }, - // validatorAddress: { - // required: true, - // }, - // }, - // }, - // exit: { - // addresses: { - // address: { - // required: true, - // network: "celo", - // }, - // }, - // args: { - // amount: { - // required: true, - // minimum: 0, - // }, - // validatorAddress: { - // required: true, - // }, - // signatureVerification: { - // required: true, - // }, - // }, - // }, - // }, - // status: { - // enter: true, - // exit: true, - // }, - // apy: 0.03992371968603679, - // rewardRate: 0.03992371968603679, - // rewardType: "apy", - // metadata: { - // cooldownPeriod: { - // days: 3, - // }, - // defaultValidator: "0xdadbd6cfb29b054adc9c4c2ef0f21f0bbdb44871", - // description: "Stake your CELO natively", - // fee: { - // enabled: false, - // }, - // gasFeeToken: { - // name: "Celo", - // symbol: "CELO", - // decimals: 18, - // address: "0x471EcE3750Da237f93B8E339c536989b8978a438", - // network: "celo", - // coinGeckoId: "celo", - // logoURI: "https://assets.stakek.it/tokens/celo.svg", - // }, - // id: "celo-celo-native-staking", - // logoURI: "https://assets.stakek.it/tokens/celo.svg", - // minimumStake: 0, - // name: "CELO Native Staking", - // revshare: { - // enabled: true, - // }, - // rewardClaiming: "auto", - // rewardSchedule: "day", - // supportsMultipleValidators: true, - // token: { - // name: "Celo", - // symbol: "CELO", - // decimals: 18, - // address: "0x471EcE3750Da237f93B8E339c536989b8978a438", - // network: "celo", - // coinGeckoId: "celo", - // logoURI: "https://assets.stakek.it/tokens/celo.svg", - // }, - // type: "staking", - // warmupPeriod: { - // days: 1, - // }, - // documentation: "https://docs.stakek.it/docs/celo-celo-native-staking", - // supportsLedgerWalletApi: true, - // isUnderMaintenance: false, - // ledgerClearSigning: true, - // contractAddresses: [ - // "0x7d21685C17607338b313a7174bAb6620baD0aaB7", - // "0x8D6677192144292870907E3Fa8A5527fE55A7ff6", - // "0x6cC083Aed9e3ebe302A6336dBC7c921C9f03349E", - // ], - // }, - // validators: [ - // { - // address: "0xe92b7ba8497486e94bb59c51f595b590c4a5f894", - // status: "active", - // name: "Stakely", - // image: "https://assets.stakek.it/validators/stakely.png", - // website: "https://stakely.io/", - // apr: 0.0393, - // commission: 0.1, - // stakedBalance: "2263157", - // votingPower: 0.0090962642447408, - // preferred: true, - // }, - // { - // address: "0x81cef0668e15639d0b101bdc3067699309d73bed", - // status: "active", - // name: "Chorus One", - // image: "https://assets.stakek.it/validators/chorus_one.png", - // website: "https://chorus.one/", - // apr: 0.04015260366943412, - // commission: 0.075, - // stakedBalance: "4056456", - // votingPower: 0.0163040370920639, - // preferred: true, - // }, - // ], - // isAvailable: true, - // }); - // }) ); - -// http.post("*/v1/actions/enter", async () => { -// await delay(); -// return HttpResponse.json( -// { -// message: "YieldUnderMaintenanceError", -// details: { yieldId: "optimism-op-aave-v3-lending" }, -// }, -// { status: 400 } -// ); -// }), - -// http.all("*", () => { -// return passthrough(); -// }) diff --git a/packages/widget/tests/fixtures/index.ts b/packages/widget/tests/fixtures/index.ts index 4b375bc8..ea543d00 100644 --- a/packages/widget/tests/fixtures/index.ts +++ b/packages/widget/tests/fixtures/index.ts @@ -7,9 +7,52 @@ import { getYieldV2ControllerGetYieldByIdResponseMock, } from "@stakekit/api-hooks/msw"; import { Just } from "purify-ts"; +import type { + YieldActionArgumentsDto, + YieldActionDto, + YieldDto as YieldApiYieldDto, + YieldBalanceDto, + YieldRewardRateDto, + YieldTransactionDto, +} from "../../src/providers/yield-api-client-provider/types"; const apyFaker = () => faker.number.float({ min: 0, max: 0.05 }); +export const yieldRewardRateFixture = ( + overrides?: Partial +): YieldRewardRateDto => ({ + total: apyFaker(), + rateType: "APY", + components: [], + ...overrides, +}); + +export const yieldApiYieldFixture = ( + overrides?: Partial +): YieldApiYieldDto => + ({ + ...getYieldV2ControllerGetYieldByIdResponseMock(), + rewardRate: yieldRewardRateFixture(), + ...overrides, + }) as YieldApiYieldDto; + +export const yieldBalanceFixture = ( + overrides?: Partial +): YieldBalanceDto => { + const token = overrides?.token ?? yieldApiYieldFixture().token; + + return { + address: faker.finance.ethereumAddress(), + type: "active", + amount: "1", + amountRaw: "1000000000000000000", + pendingActions: [], + token, + isEarning: true, + ...overrides, + } as YieldBalanceDto; +}; + export const yieldFixture = (overrides?: Partial) => Just(getYieldV2ControllerGetYieldByIdResponseMock()) .map( @@ -35,6 +78,12 @@ export const yieldFixture = (overrides?: Partial) => ) .unsafeCoerce(); +export const yieldValidatorsFixture = (validators?: YieldDto["validators"]) => + (validators ?? yieldFixture().validators).map((validator) => ({ + ...validator, + apr: validator.apr ?? apyFaker(), + })); + export const enterResponseFixture = (overrides?: Partial) => ({ ...getActionControllerEnterResponseMock(), ...overrides, @@ -51,3 +100,66 @@ export const pendingActionFixture = (overrides?: Partial) => ({ ...getActionControllerPendingResponseMock(), ...overrides, }); + +export const yieldApiTransactionFixture = ( + tx: TransactionDto, + overrides?: Partial +) => + ({ + id: tx.id || faker.string.uuid(), + title: tx.type.replaceAll("_", " "), + network: tx.network, + status: tx.status, + type: tx.type, + gasEstimate: tx.gasEstimate?.amount ?? null, + stepIndex: tx.stepIndex, + unsignedTransaction: tx.unsignedTransaction ?? undefined, + signedTransaction: tx.signedTransaction ?? undefined, + explorerUrl: tx.explorerUrl ?? undefined, + hash: tx.hash ?? undefined, + isMessage: tx.isMessage, + createdAt: tx.createdAt, + broadcastedAt: tx.broadcastedAt ?? undefined, + error: tx.error ?? undefined, + annotatedTransaction: (tx.annotatedTransaction ?? null) as never, + structuredTransaction: (tx.structuredTransaction ?? null) as never, + ...overrides, + }) as YieldTransactionDto; + +export const yieldApiActionFixture = ({ + action, + address, + rawArguments, + transactions, + overrides, +}: { + action: ActionDto; + address: string; + rawArguments?: YieldActionArgumentsDto | null; + transactions?: YieldTransactionDto[]; + overrides?: Partial; +}) => + ({ + id: action.id, + intent: + action.type === "STAKE" + ? "enter" + : action.type === "UNSTAKE" + ? "exit" + : "manage", + type: action.type, + yieldId: action.integrationId, + address, + amount: action.amount, + amountRaw: action.amount, + amountUsd: action.USDAmount, + transactions: + transactions ?? + action.transactions.map((tx) => yieldApiTransactionFixture(tx)), + executionPattern: "synchronous", + rawArguments: rawArguments ?? null, + createdAt: action.createdAt, + completedAt: action.completedAt, + status: action.status, + ...overrides, + }) as YieldActionDto; diff --git a/packages/widget/tests/use-cases/deep-links-flow/setup.ts b/packages/widget/tests/use-cases/deep-links-flow/setup.ts index abc739d7..035fee20 100644 --- a/packages/widget/tests/use-cases/deep-links-flow/setup.ts +++ b/packages/widget/tests/use-cases/deep-links-flow/setup.ts @@ -2,8 +2,15 @@ import type { TokenDto, YieldBalanceDto, YieldDto } from "@stakekit/api-hooks"; import { delay, HttpResponse, http } from "msw"; import { Just } from "purify-ts"; import { vitest } from "vitest"; +import type { YieldCreateManageActionDto } from "../../../src/providers/yield-api-client-provider/types"; import { waitForMs } from "../../../src/utils"; -import { pendingActionFixture, yieldFixture } from "../../fixtures"; +import { + pendingActionFixture, + yieldApiActionFixture, + yieldApiTransactionFixture, + yieldFixture, + yieldValidatorsFixture, +} from "../../fixtures"; import { worker } from "../../mocks/worker"; import { rkMockWallet } from "../../utils/mock-connector"; import { setUrl as _setUrl } from "./utils"; @@ -46,6 +53,24 @@ export const setup = async (opts?: { ) .unsafeCoerce(); + const avaxLiquidStakingValidators: YieldDto["validators"] = + opts?.withValidatorAddressesRequired + ? [ + { + address: "0xe92b7ba8497486e94bb59c51f595b590c4a5f894", + status: "active", + name: "Stakely", + image: "https://assets.stakek.it/validators/stakely.png", + website: "https://stakely.io/", + apr: 0.0393, + commission: 0.1, + stakedBalance: "2263157", + votingPower: 0.0090962642447408, + preferred: true, + }, + ] + : []; + const avaxLiquidStaking = Just(yieldFixture()) .map( (def) => @@ -76,22 +101,7 @@ export const setup = async (opts?: { }, ], }, - validators: opts?.withValidatorAddressesRequired - ? [ - { - address: "0xe92b7ba8497486e94bb59c51f595b590c4a5f894", - status: "active", - name: "Stakely", - image: "https://assets.stakek.it/validators/stakely.png", - website: "https://stakely.io/", - apr: 0.0393, - commission: 0.1, - stakedBalance: "2263157", - votingPower: 0.0090962642447408, - preferred: true, - }, - ] - : [], + validators: avaxLiquidStakingValidators, }) satisfies YieldDto ) .unsafeCoerce(); @@ -120,6 +130,19 @@ export const setup = async (opts?: { }, ]; + const avaxLiquidStakingBalancesV2 = [ + { + address: account, + type: "claimable", + amount: "0.019258000000000000", + amountRaw: "19258000000000000", + pendingActions: avaxLiquidStakingBalances[0].pendingActions, + token: avaxLiquidStaking.token, + amountUsd: "0.84", + isEarning: false, + }, + ]; + const pendingAction = Just(pendingActionFixture()) .map((def): typeof def => ({ ...def, @@ -193,6 +216,22 @@ export const setup = async (opts?: { await delay(); return HttpResponse.json(avaxLiquidStaking); }), + http.get("*/v1/yields/:yieldId/validators", async (info) => { + await delay(); + + const yieldId = info.params.yieldId as string; + const validators = + yieldId === avaxLiquidStaking.id + ? yieldValidatorsFixture(avaxLiquidStakingValidators) + : yieldId === avaxNativeStaking.id + ? yieldValidatorsFixture(avaxNativeStaking.validators) + : []; + + return HttpResponse.json({ + items: validators, + total: validators.length, + }); + }), http.post("*/v1/yields/balances/scan", async () => { await delay(); return HttpResponse.json([ @@ -202,6 +241,18 @@ export const setup = async (opts?: { }, ]); }), + http.post("*/v1/yields/balances", async () => { + await delay(); + return HttpResponse.json({ + items: [ + { + yieldId: avaxLiquidStaking.id, + balances: avaxLiquidStakingBalancesV2, + }, + ], + errors: [], + }); + }), http.post(`*/v1/yields/${avaxNativeStaking.id}/balances/scan`, async () => { await delay(); return HttpResponse.json({ @@ -211,40 +262,60 @@ export const setup = async (opts?: { }), http.post(`*/v1/yields/${avaxLiquidStaking.id}/balances`, async () => { await delay(); - return HttpResponse.json(avaxLiquidStakingBalances); + return HttpResponse.json({ + yieldId: avaxLiquidStaking.id, + balances: avaxLiquidStakingBalancesV2, + }); }), - http.post("*/v1/actions/pending", async (info) => { - const data = (await info.request.json()) as { integrationId: string }; + http.post("*/v1/actions/manage", async (info) => { + const data = (await info.request.json()) as YieldCreateManageActionDto; await delay(); + return HttpResponse.json({ - ...pendingAction, - integrationId: data.integrationId, - } satisfies typeof pendingAction); + ...yieldApiActionFixture({ + action: pendingAction, + address: data.address, + rawArguments: data.arguments ?? null, + transactions: [ + yieldApiTransactionFixture(pendingAction.transactions[0], { + type: "CLAIM_REWARDS", + status: "CREATED", + unsignedTransaction: + '{"from":"0xcaA141ece9fEE66D15f0257F5c6C48E26784345C","gasLimit":"0x0193e0","to":"0x7D2382b1f8Af621229d33464340541Db362B4907","data":"0x00f714ce00000000000000000000000000000000000000000000000000037cb07e6e4276000000000000000000000000caa141ece9fee66d15f0257f5c6c48e26784345c","nonce":89,"type":2,"maxFeePerGas":"0xbfa6de","maxPriorityFeePerGas":"0x0f4240","chainId":43114}', + }), + ], + overrides: { + yieldId: data.yieldId, + }, + }), + }); }), - http.patch("*/v1/transactions/:transactionId", async (info) => { + http.put("*/v1/transactions/:transactionId/submit-hash", async (info) => { await delay(); const transactionId = info.params.transactionId as string; return HttpResponse.json({ - ...pendingAction.transactions[0], - type: "CLAIM_REWARDS", - status: "WAITING_FOR_SIGNATURE", - id: transactionId, - unsignedTransaction: - '{"from":"0xcaA141ece9fEE66D15f0257F5c6C48E26784345C","gasLimit":"0x0193e0","to":"0x7D2382b1f8Af621229d33464340541Db362B4907","data":"0x00f714ce00000000000000000000000000000000000000000000000000037cb07e6e4276000000000000000000000000caa141ece9fee66d15f0257f5c6c48e26784345c","nonce":89,"type":2,"maxFeePerGas":"0xbfa6de","maxPriorityFeePerGas":"0x0f4240","chainId":43114}', + ...yieldApiTransactionFixture(pendingAction.transactions[0], { + type: "CLAIM_REWARDS", + status: "BROADCASTED", + id: transactionId, + hash: "transaction_hash", + }), }); }), - http.post("*/v1/transactions/:transactionId/submit_hash", async () => { - await delay(1000); - return new HttpResponse(null, { status: 201 }); - }), - http.get("*/v1/transactions/:transactionId/status", async () => { + http.get("*/v1/transactions/:transactionId", async (info) => { + const transactionId = info.params.transactionId as string; return HttpResponse.json({ - url: "https://snowtrace.dev/tx/0x5c2e4ac81fa12b8e935e1cf5e39eda4594d75e82da0c9b44c6d85f20214452fb", - network: avaxLiquidStaking.token.network, - hash: "0x5c2e4ac81fa12b8e935e1cf5e39eda4594d75e82da0c9b44c6d85f20214452fb", - status: "CONFIRMED", + ...yieldApiTransactionFixture(pendingAction.transactions[0], { + id: transactionId, + type: "CLAIM_REWARDS", + explorerUrl: + "https://snowtrace.dev/tx/0x5c2e4ac81fa12b8e935e1cf5e39eda4594d75e82da0c9b44c6d85f20214452fb", + network: avaxLiquidStaking.token.network, + hash: "0x5c2e4ac81fa12b8e935e1cf5e39eda4594d75e82da0c9b44c6d85f20214452fb", + status: "CONFIRMED", + }), }); }) ); diff --git a/packages/widget/tests/use-cases/external-provider/setup.ts b/packages/widget/tests/use-cases/external-provider/setup.ts index 40767911..706816ec 100644 --- a/packages/widget/tests/use-cases/external-provider/setup.ts +++ b/packages/widget/tests/use-cases/external-provider/setup.ts @@ -1,7 +1,7 @@ import type { TokenDto, YieldDto } from "@stakekit/api-hooks"; import { delay, HttpResponse, http } from "msw"; import { Just } from "purify-ts"; -import { yieldFixture } from "../../fixtures"; +import { yieldFixture, yieldValidatorsFixture } from "../../fixtures"; import { worker } from "../../mocks/worker"; export const setup = () => { @@ -49,6 +49,7 @@ export const setup = () => { id: "avalanche-avax-native-staking", token: avalancheCToken, tokens: [avalancheCToken], + validators: [], metadata: { ...val.metadata, type: "staking", @@ -66,6 +67,7 @@ export const setup = () => { id: "ethereum-eth-etherfi-staking", token: ether, tokens: [ether], + validators: [], metadata: { ...val.metadata, type: "staking", @@ -83,6 +85,7 @@ export const setup = () => { id: "solana-sol-native-staking", token: solanaToken, tokens: [solanaToken], + validators: [], metadata: { ...val.metadata, type: "staking", @@ -100,6 +103,7 @@ export const setup = () => { id: "ton-native-staking", token: tonToken, tokens: [tonToken], + validators: [], metadata: { ...val.metadata, type: "staking", @@ -179,6 +183,25 @@ export const setup = () => { await delay(); return HttpResponse.json(tonNativeStaking); + }), + http.get("*/v1/yields/:yieldId/validators", async (info) => { + await delay(); + + const yieldId = info.params.yieldId as string; + const validatorsByYieldId = new Map([ + [etherNativeStaking.id, etherNativeStaking.validators], + [avalancheAvaxNativeStaking.id, avalancheAvaxNativeStaking.validators], + [solanaNativeStaking.id, solanaNativeStaking.validators], + [tonNativeStaking.id, tonNativeStaking.validators], + ]); + const validators = yieldValidatorsFixture( + validatorsByYieldId.get(yieldId) ?? [] + ); + + return HttpResponse.json({ + items: validators, + total: validators.length, + }); }) ); }; diff --git a/packages/widget/tests/use-cases/gas-warning-flow/setup.ts b/packages/widget/tests/use-cases/gas-warning-flow/setup.ts index dd16f71a..46c4525d 100644 --- a/packages/widget/tests/use-cases/gas-warning-flow/setup.ts +++ b/packages/widget/tests/use-cases/gas-warning-flow/setup.ts @@ -1,6 +1,5 @@ import type { ActionDto, - ActionRequestDto, TokenDto, TransactionDto, YieldDto, @@ -8,11 +7,15 @@ import type { import { delay, HttpResponse, http } from "msw"; import { Just } from "purify-ts"; import { vitest } from "vitest"; +import type { YieldCreateActionDto } from "../../../src/providers/yield-api-client-provider/types"; import { waitForMs } from "../../../src/utils"; import { enterResponseFixture, transactionConstructFixture, + yieldApiActionFixture, + yieldApiTransactionFixture, yieldFixture, + yieldValidatorsFixture, } from "../../fixtures"; import { worker } from "../../mocks/worker"; import { rkMockWallet } from "../../utils/mock-connector"; @@ -43,6 +46,7 @@ export const setup = () => { id: "avalanche-avax-native-staking", token: avalancheCToken, tokens: [avalancheCToken], + validators: [], metadata: { ...val.metadata, type: "staking", @@ -80,6 +84,7 @@ export const setup = () => { id: "avalanche-c-usdc-aave-v3-lending", token: usdcToken, tokens: [usdcToken], + validators: [], metadata: { ...val.metadata, type: "staking", @@ -197,62 +202,51 @@ export const setup = () => { return HttpResponse.json(yieldWithDifferentGasAndStakeToken.yieldDto); } ), - http.post("*/v1/actions/enter", async (info) => { + http.get("*/v1/yields/:yieldId/validators", async (info) => { await delay(); - const body = (await info.request.json()) as ActionRequestDto; + const yieldId = info.params.yieldId as string; + const validators = + yieldId === yieldWithSameGasAndStakeToken.yieldDto.id + ? yieldValidatorsFixture( + yieldWithSameGasAndStakeToken.yieldDto.validators + ) + : yieldValidatorsFixture( + yieldWithDifferentGasAndStakeToken.yieldDto.validators + ); return HttpResponse.json({ - ...(body.integrationId === yieldWithSameGasAndStakeToken.yieldDto.id - ? yieldWithSameGasAndStakeToken.actionDto - : yieldWithDifferentGasAndStakeToken.actionDto), - amount: body.args.amount, - } as ActionDto); - }), - http.patch("*/v1/transactions/:transactionId", async (info) => { - const transactionId = info.params.transactionId as string; - - const yieldWithAction = [ - yieldWithSameGasAndStakeToken, - yieldWithDifferentGasAndStakeToken, - ].find((val) => - val.actionDto.transactions.some((tx) => tx.id === transactionId) - ); - - if (!yieldWithAction) { - return new HttpResponse(null, { status: 400 }); - } - - const tx = yieldWithAction.actionDto.transactions.find( - (tx) => tx.id === transactionId - ); - - if (!tx) { - return new HttpResponse(null, { status: 400 }); - } - - await delay(); - - return HttpResponse.json({ - ...tx, - gasEstimate: { - token: yieldWithAction.yieldDto.token, - amount: - yieldsTxGasAmountMap.get( - `${yieldWithAction.yieldDto.id}-${tx.id}` - ) ?? "0", - }, - } satisfies TransactionDto); + items: validators, + total: validators.length, + }); }), - http.post("*/v1/actions/enter/estimate-gas", async (info) => { + http.post("*/v1/actions/enter", async (info) => { await delay(); - const body = (await info.request.json()) as ActionRequestDto; + const body = (await info.request.json()) as YieldCreateActionDto; + const selectedYield = + body.yieldId === yieldWithSameGasAndStakeToken.yieldDto.id + ? yieldWithSameGasAndStakeToken + : yieldWithDifferentGasAndStakeToken; + const gasAmount = yieldsTxGasAmountMap.get(body.yieldId) ?? "0"; return HttpResponse.json({ - amount: yieldsTxGasAmountMap.get(body.integrationId) ?? "0", - token: avalancheCToken, - gasLimit: "", + ...yieldApiActionFixture({ + action: selectedYield.actionDto as ActionDto, + address: body.address, + rawArguments: body.arguments ?? null, + transactions: selectedYield.actionDto.transactions.map((tx, index) => + yieldApiTransactionFixture(tx as TransactionDto, { + gasEstimate: gasAmount, + status: "CREATED", + stepIndex: index, + }) + ), + overrides: { + amount: body.arguments?.amount ?? null, + amountRaw: body.arguments?.amount ?? null, + }, + }), }); }) ); diff --git a/packages/widget/tests/use-cases/staking-flow/setup.ts b/packages/widget/tests/use-cases/staking-flow/setup.ts index f3727aed..b4ac4d6b 100644 --- a/packages/widget/tests/use-cases/staking-flow/setup.ts +++ b/packages/widget/tests/use-cases/staking-flow/setup.ts @@ -1,6 +1,5 @@ import type { ActionDto, - ActionRequestDto, AddressesDto, TokenDto, TransactionDto, @@ -9,7 +8,13 @@ import type { import { delay, HttpResponse, http } from "msw"; import { avalanche } from "viem/chains"; import { vitest } from "vitest"; +import type { YieldCreateActionDto } from "../../../src/providers/yield-api-client-provider/types"; import { waitForMs } from "../../../src/utils"; +import { + yieldApiActionFixture, + yieldApiTransactionFixture, + yieldValidatorsFixture, +} from "../../fixtures"; import { worker } from "../../mocks/worker"; import { rkMockWallet } from "../../utils/mock-connector"; @@ -245,6 +250,20 @@ export const setup = async () => { await delay(); return HttpResponse.json(yieldOp); }), + http.get("*/v1/yields/:yieldId/validators", async (info) => { + await delay(); + + const yieldId = info.params.yieldId as string; + const validators = + yieldId === yieldOp.id + ? yieldValidatorsFixture(yieldOp.validators) + : []; + + return HttpResponse.json({ + items: validators, + total: validators.length, + }); + }), http.get("*/v1/transactions/gas/avalanche-c", async () => { await delay(); return HttpResponse.json({ @@ -286,46 +305,57 @@ export const setup = async () => { }, }); }), - http.post("*/v1/actions/enter/estimate-gas", async () => { - await delay(); - return HttpResponse.json({ - amount: "0.002828600000000000", - token: { - network: "polygon", - coinGeckoId: "matic-network", - name: "Polygon", - decimals: 18, - symbol: "MATIC", - logoURI: "https://assets.stakek.it/tokens/matic.svg", - }, - gasLimit: "", - }); - }), http.post("*/v1/actions/enter", async (info) => { await delay(); - const body = (await info.request.json()) as ActionRequestDto; + const body = (await info.request.json()) as YieldCreateActionDto; - return HttpResponse.json({ ...enterAction, amount: body.args.amount }); + return HttpResponse.json( + yieldApiActionFixture({ + action: enterAction, + address: body.address, + rawArguments: body.arguments ?? null, + transactions: [ + yieldApiTransactionFixture(transactionConstruct, { + id: enterAction.transactions[0].id, + status: "CREATED", + type: "STAKE", + gasEstimate: + transactionConstruct.gasEstimate?.amount ?? undefined, + stepIndex: 0, + }), + ], + overrides: { + amount: body.arguments?.amount ?? null, + amountRaw: body.arguments?.amount ?? null, + }, + }) + ); }), - http.patch("*/v1/transactions/:transactionId", async (info) => { + http.put("*/v1/transactions/:transactionId/submit-hash", async (info) => { const transactionId = info.params.transactionId as string; await delay(); - return HttpResponse.json({ ...transactionConstruct, id: transactionId }); - }), - http.post("*/v1/transactions/:transactionId/submit_hash", async () => { - await delay(1000); - return new HttpResponse(null, { status: 201 }); + return HttpResponse.json( + yieldApiTransactionFixture(transactionConstruct, { + id: transactionId, + hash: "transaction_hash", + status: "BROADCASTED", + }) + ); }), - http.get("*/v1/transactions/:transactionId/status", async () => { - return HttpResponse.json({ - url: "https://snowtrace.dev/tx/0x5c2e4ac81fa12b8e935e1cf5e39eda4594d75e82da0c9b44c6d85f20214452fb", - network: "avalanche-c", - hash: "0x5c2e4ac81fa12b8e935e1cf5e39eda4594d75e82da0c9b44c6d85f20214452fb", - status: "CONFIRMED", - }); + http.get("*/v1/transactions/:transactionId", async (info) => { + const transactionId = info.params.transactionId as string; + return HttpResponse.json( + yieldApiTransactionFixture(transactionConstruct, { + id: transactionId, + explorerUrl: + "https://snowtrace.dev/tx/0x5c2e4ac81fa12b8e935e1cf5e39eda4594d75e82da0c9b44c6d85f20214452fb", + hash: "0x5c2e4ac81fa12b8e935e1cf5e39eda4594d75e82da0c9b44c6d85f20214452fb", + status: "CONFIRMED", + }) + ); }) ); diff --git a/packages/widget/tests/use-cases/trust-incentive-apy/setup.ts b/packages/widget/tests/use-cases/trust-incentive-apy/setup.ts new file mode 100644 index 00000000..cc6302cb --- /dev/null +++ b/packages/widget/tests/use-cases/trust-incentive-apy/setup.ts @@ -0,0 +1,334 @@ +import type { TokenDto, YieldDto } from "@stakekit/api-hooks"; +import { delay, HttpResponse, http } from "msw"; +import { avalanche } from "viem/chains"; +import { vitest } from "vitest"; +import type { + YieldDto as YieldApiYieldDto, + YieldBalanceDto, + YieldRewardRateDto, +} from "../../../src/providers/yield-api-client-provider/types"; +import { waitForMs } from "../../../src/utils"; +import { + yieldApiYieldFixture, + yieldBalanceFixture, + yieldFixture, + yieldRewardRateFixture, +} from "../../fixtures"; +import { worker } from "../../mocks/worker"; +import { rkMockWallet } from "../../utils/mock-connector"; + +const setUrl = ({ + accountId, + balanceId, + yieldId, +}: { + accountId?: string; + balanceId?: string; + yieldId?: string; +}) => { + const searchParams = new URLSearchParams(); + + if (accountId) { + searchParams.set("accountId", accountId); + } + + if (balanceId) { + searchParams.set("balanceId", balanceId); + } + + if (yieldId) { + searchParams.set("yieldId", yieldId); + } + + const url = new URL(window.location.href); + url.search = searchParams.toString(); + window.history.pushState({}, "", url); +}; + +export const setup = async () => { + const account = "0xB6c5273e79E2aDD234EBC07d87F3824e0f94B2F7"; + + const token: TokenDto = { + name: "USDA", + symbol: "USDA", + decimals: 18, + network: "avalanche-c", + coinGeckoId: "angle-usd", + logoURI: "https://assets.stakek.it/tokens/usda.svg", + }; + + const rewardToken: TokenDto = { + name: "United Stables", + symbol: "U", + decimals: 18, + network: token.network, + address: "0x58D97B57BB95320F9a05dC918Aef65434969c2B2", + logoURI: "https://assets.stakek.it/tokens/usda.svg", + }; + + const morphoToken: TokenDto = { + name: "Morpho Token", + symbol: "MORPHO", + decimals: 18, + network: token.network, + address: "0x58D97B57BB95320F9a05dC918Aef65434969c2B3", + logoURI: "https://assets.stakek.it/tokens/usda.svg", + }; + + const discoveryRewardRate: YieldRewardRateDto = yieldRewardRateFixture({ + total: 0.045507546653006034, + rateType: "APY", + components: [ + { + rate: 0.0028386677110199426, + rateType: "APR", + token: morphoToken, + yieldSource: "protocol_incentive", + description: "MORPHO rewards", + }, + { + rate: 0.002, + rateType: "APR", + token: rewardToken, + yieldSource: "campaign_incentive", + description: "U rewards", + }, + { + rate: 0.042668878941986094, + rateType: "APY", + token: rewardToken, + yieldSource: "vault", + description: "Supply APY", + }, + ], + }); + + const personalizedRewardRate: YieldRewardRateDto = yieldRewardRateFixture({ + total: 0.04530754665300604, + rateType: "APY", + components: [ + { + rate: 0.0028386677110199426, + rateType: "APR", + token: morphoToken, + yieldSource: "protocol_incentive", + description: "MORPHO rewards", + }, + { + rate: 0.0018, + rateType: "APR", + token: rewardToken, + yieldSource: "campaign_incentive", + description: "U rewards", + }, + { + rate: 0.042668878941986094, + rateType: "APY", + token: rewardToken, + yieldSource: "vault", + description: "Supply APY", + }, + ], + }); + + const yieldId = + "avalanche-c-usda-trust-0xbeefa1abfebe621df50ceaef9f54fdb73648c92c-vault"; + + const legacyYieldBase = yieldFixture(); + const rawYieldBase = yieldApiYieldFixture(); + + const legacyYield: YieldDto = { + ...legacyYieldBase, + id: yieldId, + token, + tokens: [token], + rewardRate: discoveryRewardRate.total, + rewardType: "apy", + apy: discoveryRewardRate.total, + validators: [], + feeConfigurations: [], + args: { + enter: { + args: { + amount: { + required: true, + minimum: 0, + }, + }, + }, + exit: { + args: { + amount: { + required: true, + minimum: 0, + }, + }, + }, + }, + metadata: { + ...legacyYieldBase.metadata, + name: "Trust USDA Earn", + type: "vault", + token, + rewardTokens: undefined, + gasFeeToken: token, + provider: { + id: legacyYieldBase.metadata.provider?.id ?? "benqi", + name: "Trust", + description: "", + externalLink: "https://trustwallet.com", + logoURI: "https://assets.stakek.it/providers/benqi.svg", + }, + }, + status: { + enter: true, + exit: true, + }, + }; + + const rawYield: YieldApiYieldDto = { + ...rawYieldBase, + id: yieldId, + token, + tokens: [token], + inputTokens: [token], + outputToken: { + ...token, + symbol: "steakUSDA", + name: "Steakhouse USDA", + }, + network: token.network, + chainId: `${avalanche.id}`, + providerId: rawYieldBase.providerId, + rewardRate: discoveryRewardRate, + metadata: { + ...(rawYieldBase.metadata ?? {}), + name: "Trust USDA Earn", + description: "Trust campaign vault", + logoURI: "https://assets.stakek.it/providers/benqi.svg", + }, + mechanics: { + ...(rawYieldBase.mechanics ?? {}), + type: "vault", + gasFeeToken: token, + rewardClaiming: "auto", + rewardSchedule: "day", + requiresValidatorSelection: false, + supportsLedgerWalletApi: true, + }, + status: { + enter: true, + exit: true, + }, + }; + + const activeBalance: YieldBalanceDto = yieldBalanceFixture({ + address: account, + type: "active", + amount: "1000251.8279906842", + amountRaw: "1000251827990684200000000", + amountUsd: "1000355.009527", + isEarning: true, + token, + pendingActions: [], + }); + + worker.use( + http.get("*/v1/yields/enabled/networks", async () => { + await delay(); + return HttpResponse.json([token.network]); + }), + http.get("*/v1/tokens", async () => { + await delay(); + return HttpResponse.json([ + { + token, + availableYields: [yieldId], + }, + ]); + }), + http.post("*/v1/tokens/balances/scan", async () => { + await delay(); + return HttpResponse.json([ + { + token, + amount: "1000251.8279906842", + availableYields: [yieldId], + }, + ]); + }), + http.post("*/v1/tokens/balances", async () => { + await delay(); + return HttpResponse.json([ + { + token, + amount: "1000251.8279906842", + }, + ]); + }), + http.post("*/v1/tokens/prices", async () => { + await delay(); + return HttpResponse.json({ + "avalanche-c-undefined": { + price: 1, + price_24_h: 0, + }, + }); + }), + http.get(`*/v1/yields/${yieldId}`, async ({ request }) => { + await delay(); + + const url = new URL(request.url); + + return HttpResponse.json( + url.searchParams.has("ledgerWalletAPICompatible") + ? legacyYield + : rawYield + ); + }), + http.get("*/v1/yields/:yieldId/validators", async () => { + await delay(); + return HttpResponse.json({ + items: [], + total: 0, + }); + }), + http.post("*/v1/yields/balances", async () => { + await delay(); + return HttpResponse.json({ + items: [ + { + yieldId, + balances: [activeBalance], + rewardRate: personalizedRewardRate, + }, + ], + errors: [], + }); + }) + ); + + 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 customConnectors = rkMockWallet({ accounts: [account], requestFn }); + + return { + account, + customConnectors, + setUrl, + legacyYield, + discoveryRewardRate, + personalizedRewardRate, + }; +}; diff --git a/packages/widget/tests/use-cases/trust-incentive-apy/trust-incentive-apy.test.tsx b/packages/widget/tests/use-cases/trust-incentive-apy/trust-incentive-apy.test.tsx new file mode 100644 index 00000000..d955411d --- /dev/null +++ b/packages/widget/tests/use-cases/trust-incentive-apy/trust-incentive-apy.test.tsx @@ -0,0 +1,105 @@ +import { describe, expect, it } from "vitest"; +import { renderApp } from "../../utils/test-utils"; +import { setup } from "./setup"; + +describe("Trust incentive APY", () => { + it("shows APY composition during discovery", async () => { + const { account, customConnectors, legacyYield, setUrl } = await setup(); + + setUrl({ + accountId: account, + yieldId: legacyYield.id, + }); + + const app = await renderApp({ + wagmi: { + __customConnectors__: customConnectors, + }, + }); + + await expect + .element(app.getByTestId("estimated-reward__percent").getByText("4.55%")) + .toBeInTheDocument(); + + await expect.element(app.getByText("APY composition")).toBeInTheDocument(); + await expect + .element( + app.getByTestId("reward-rate-breakdown__native").getByText("4.27%") + ) + .toBeInTheDocument(); + await expect + .element( + app + .getByTestId("reward-rate-breakdown__protocol-incentive") + .getByText("0.28%") + ) + .toBeInTheDocument(); + await expect + .element( + app + .getByTestId("reward-rate-breakdown__campaign") + .getByText("Up to 0.20%") + ) + .toBeInTheDocument(); + + await app.getByTestId("select-opportunity").click(); + + const selectContainer = app.getByTestId("select-modal__container"); + + await expect + .element(selectContainer.getByText("Trust USDA Earn")) + .toBeInTheDocument(); + await expect + .element(selectContainer.getByText("4.55%")) + .toBeInTheDocument(); + await expect + .element(selectContainer.getByText("Up to 0.20%")) + .toBeInTheDocument(); + + app.unmount(); + }); + + it("shows personalized APY on the position details page", async () => { + const { account, customConnectors, legacyYield, setUrl } = await setup(); + + setUrl({ + accountId: account, + balanceId: "default", + yieldId: legacyYield.id, + }); + + const app = await renderApp({ + wagmi: { + __customConnectors__: customConnectors, + }, + }); + + await expect.element(app.getByText("Personalized APY")).toBeInTheDocument(); + await expect + .element(app.getByTestId("personalized-reward-rate").getByText("4.53%")) + .toBeInTheDocument(); + await expect + .element( + app + .getByTestId("personalized-reward-rate-breakdown__native") + .getByText("4.27%") + ) + .toBeInTheDocument(); + await expect + .element( + app + .getByTestId("personalized-reward-rate-breakdown__protocol-incentive") + .getByText("0.28%") + ) + .toBeInTheDocument(); + await expect + .element( + app + .getByTestId("personalized-reward-rate-breakdown__campaign") + .getByText("0.18%") + ) + .toBeInTheDocument(); + + app.unmount(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6db09c3a..05be648b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -320,6 +320,15 @@ importers: msw: specifier: ^2.12.4 version: 2.12.4(@types/node@25.0.2)(typescript@5.9.3) + openapi-fetch: + specifier: ^0.17.0 + version: 0.17.0 + openapi-react-query: + specifier: ^0.5.4 + version: 0.5.4(@tanstack/react-query@5.90.12(react@19.2.3))(openapi-fetch@0.17.0) + openapi-typescript: + specifier: ^7.13.0 + version: 7.13.0(typescript@5.9.3) playwright: specifier: ^1.57.0 version: 1.57.0 @@ -3140,6 +3149,16 @@ packages: '@types/react': optional: true + '@redocly/ajv@8.17.4': + resolution: {integrity: sha512-BieiCML/IgP6x99HZByJSt7fJE4ipgzO7KAFss92Bs+PEI35BhY7vGIysFXLT+YmS7nHtQjZjhOQyPPEf7xGHA==} + + '@redocly/config@0.22.2': + resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + + '@redocly/openapi-core@1.34.7': + resolution: {integrity: sha512-gn2P0OER6qxF/+f4GqNv9XsnU5+6oszD/0SunulOvPYJDhrNkNVrVZV5waX25uqw5UDn2+roViWlRDHKFfHH0g==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@reown/appkit-common@1.7.2': resolution: {integrity: sha512-DZkl3P5+Iw3TmsitWmWxYbuSCox8iuzngNp/XhbNDJd7t4Cj4akaIUxSEeCajNDiGHlu4HZnfyM1swWsOJ0cOw==} @@ -5915,6 +5934,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -6011,6 +6033,9 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -6871,7 +6896,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -7085,6 +7110,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -7373,6 +7402,10 @@ packages: js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + js-sha3@0.8.0: resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} @@ -7797,6 +7830,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.7: + resolution: {integrity: sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==} + engines: {node: '>=10'} + minimatch@7.4.6: resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} engines: {node: '>=10'} @@ -8165,9 +8202,27 @@ packages: openapi-fetch@0.13.8: resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} + openapi-fetch@0.17.0: + resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} + + openapi-react-query@0.5.4: + resolution: {integrity: sha512-V9lRiozjHot19/BYSgXYoyznDxDJQhEBSdi26+SJ0UqjMANLQhkni4XG+Z7e3Ag7X46ZLMrL9VxYkghU3QvbWg==} + peerDependencies: + '@tanstack/react-query': ^5.80.0 + openapi-fetch: ^0.17.0 + openapi-typescript-helpers@0.0.15: resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + openapi-typescript-helpers@0.1.0: + resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} + + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + ora@8.2.0: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} @@ -8301,6 +8356,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -8433,6 +8492,10 @@ packages: please-upgrade-node@3.2.0: resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -9340,6 +9403,10 @@ packages: resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} engines: {node: '>=14.0.0'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -9564,6 +9631,10 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + type-fest@5.3.1: resolution: {integrity: sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==} engines: {node: '>=20'} @@ -10091,6 +10162,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} @@ -10289,6 +10361,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -10474,7 +10549,7 @@ snapshots: '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -10644,7 +10719,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/types': 7.28.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -13022,7 +13097,7 @@ snapshots: '@scure/base': 1.2.6 '@types/debug': 4.1.12 '@types/lodash': 4.17.21 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) lodash: 4.17.21 pony-cause: 2.1.11 semver: 7.7.3 @@ -13035,7 +13110,7 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) semver: 7.7.3 superstruct: 1.0.4 transitivePeerDependencies: @@ -13048,7 +13123,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 @@ -13062,7 +13137,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 @@ -14028,7 +14103,7 @@ snapshots: '@react-native/community-cli-plugin@0.81.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@react-native/dev-middleware': 0.81.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) invariant: 2.2.4 metro: 0.83.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) metro-config: 0.83.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -14048,7 +14123,7 @@ snapshots: chrome-launcher: 0.15.2 chromium-edge-launcher: 0.2.0 connect: 3.7.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 @@ -14074,6 +14149,29 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@redocly/ajv@8.17.4': + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + '@redocly/config@0.22.2': {} + + '@redocly/openapi-core@1.34.7(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.17.4 + '@redocly/config': 0.22.2 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.1.1 + minimatch: 5.1.7 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@reown/appkit-common@1.7.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: big.js: 6.2.2 @@ -17206,7 +17304,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.13.1 '@typescript-eslint/visitor-keys': 7.13.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -20108,6 +20206,8 @@ snapshots: chalk@5.6.2: {} + change-case@5.4.4: {} + chardet@2.1.1: {} charenc@0.0.2: @@ -20212,6 +20312,8 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + colorette@1.4.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -20463,9 +20565,11 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 decamelize@1.2.0: {} @@ -20528,7 +20632,7 @@ snapshots: callsite: 1.0.0 camelcase: 6.3.0 cosmiconfig: 7.1.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) deps-regex: 0.2.0 findup-sync: 5.0.0 ignore: 5.3.2 @@ -21315,17 +21419,17 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color optional: true https-browserify@1.0.0: {} - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@10.2.2): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -21392,6 +21496,8 @@ snapshots: imurmurhash@0.1.4: {} + index-to-position@1.2.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -21692,6 +21798,8 @@ snapshots: js-base64@3.7.8: {} + js-levenshtein@1.1.6: {} + js-sha3@0.8.0: {} js-tokens@4.0.0: {} @@ -21725,7 +21833,7 @@ snapshots: decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) is-potential-custom-element-name: 1.0.1 parse5: 8.0.0 saxes: 6.0.0 @@ -22056,7 +22164,7 @@ snapshots: dependencies: exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) metro-core: 0.83.3 transitivePeerDependencies: - supports-color @@ -22084,7 +22192,7 @@ snapshots: metro-file-map@0.83.3: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -22180,7 +22288,7 @@ snapshots: chalk: 4.1.2 ci-info: 2.0.0 connect: 3.7.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -22256,6 +22364,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@5.1.7: + dependencies: + brace-expansion: 2.0.2 + minimatch@7.4.6: dependencies: brace-expansion: 2.0.2 @@ -22565,9 +22677,31 @@ snapshots: openapi-typescript-helpers: 0.0.15 optional: true + openapi-fetch@0.17.0: + dependencies: + openapi-typescript-helpers: 0.1.0 + + openapi-react-query@0.5.4(@tanstack/react-query@5.90.12(react@19.2.3))(openapi-fetch@0.17.0): + dependencies: + '@tanstack/react-query': 5.90.12(react@19.2.3) + openapi-fetch: 0.17.0 + openapi-typescript-helpers: 0.1.0 + openapi-typescript-helpers@0.0.15: optional: true + openapi-typescript-helpers@0.1.0: {} + + openapi-typescript@7.13.0(typescript@5.9.3): + dependencies: + '@redocly/openapi-core': 1.34.7(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 + yargs-parser: 21.1.1 + ora@8.2.0: dependencies: chalk: 5.6.2 @@ -22783,6 +22917,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.2.0 + type-fest: 4.41.0 + parse-node-version@1.0.1: optional: true @@ -22916,6 +23056,8 @@ snapshots: dependencies: semver-compare: 1.0.0 + pluralize@8.0.0: {} + pngjs@5.0.0: {} pngjs@7.0.0: {} @@ -23835,7 +23977,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -24001,6 +24143,8 @@ snapshots: superstruct@2.0.2: {} + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -24202,6 +24346,8 @@ snapshots: type-fest@2.19.0: {} + type-fest@4.41.0: {} + type-fest@5.3.1: dependencies: tagged-tag: 1.0.0 @@ -24569,7 +24715,7 @@ snapshots: vite-node@3.2.4(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.2.7(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) @@ -24912,6 +25058,8 @@ snapshots: yallist@3.1.1: {} + yaml-ast-parser@0.0.43: {} + yaml@1.10.2: {} yaml@2.8.2: {}