diff --git a/package.json b/package.json index 9487ca9e..43eb5ded 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,5 @@ "husky": "catalog:", "rev-dep": "catalog:", "turbo": "catalog:" - }, - "pnpm": { - "patchedDependencies": { - "purify-ts": "patches/purify-ts.patch", - "@stakekit/rainbowkit@2.2.11": "patches/@stakekit__rainbowkit@2.2.11.patch" - } } } diff --git a/packages/widget/package.json b/packages/widget/package.json index c26d1b74..4458d1d4 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -75,6 +75,7 @@ "@cosmos-kit/keplr": "catalog:", "@cosmos-kit/leap": "catalog:", "@cosmos-kit/walletconnect": "catalog:", + "@effect/atom-react": "catalog:", "@effect/openapi-generator": "catalog:", "@effect/platform-node": "catalog:", "@faker-js/faker": "catalog:", @@ -117,7 +118,6 @@ "@vitejs/plugin-react": "catalog:", "@vitest/browser-playwright": "catalog:", "@xstate/react": "catalog:", - "@xstate/store": "catalog:", "autoprefixer": "catalog:", "babel-plugin-react-compiler": "catalog:", "bignumber.js": "catalog:", diff --git a/packages/widget/src/Dashboard.tsx b/packages/widget/src/Dashboard.tsx index 91fb2ca0..0fb2240d 100644 --- a/packages/widget/src/Dashboard.tsx +++ b/packages/widget/src/Dashboard.tsx @@ -6,7 +6,6 @@ import { PendingCompletePage } from "./pages/complete/pages/pending-complete.pag import { StakeCompletePage } from "./pages/complete/pages/stake-complete.page"; import { UnstakeCompletePage } from "./pages/complete/pages/unstake-complete.page"; import { EarnPageContextProvider } from "./pages/details/earn-page/state/earn-page-context"; -import { EarnPageStateUsageBoundaryProvider } from "./pages/details/earn-page/state/earn-page-state-context"; import { StakeReviewPage } from "./pages/review"; import { PendingReviewPage } from "./pages/review/pages/pending-review.page"; import { UnstakeReviewPage } from "./pages/review/pages/unstake-review.page"; @@ -40,77 +39,73 @@ export const Dashboard = () => { return ( - - - - }> - {/* Earn Tab */} - }> - } /> + + + }> + {/* Earn Tab */} + }> + } /> - }> - } /> - } /> - } /> - + }> + } /> + } /> + } /> + - {/* Manage Tab */} - } /> + {/* Manage Tab */} + } /> - {/* Position Details */} - } - > - } /> + {/* Position Details */} + } + > + } /> - {/* Staking */} - - } /> - } /> - } /> - } /> - + {/* Staking */} + + } /> + } /> + } /> + } /> + - } - /> + } + /> - {/* Unstaking */} - - } /> - } /> - } /> - } /> - + {/* Unstaking */} + + } /> + } /> + } /> + } /> + - {/* Pending Actions */} - - } /> - } /> - } /> - + {/* Pending Actions */} + + } /> + } /> + } /> + - {/* Rewards Tab */} - {/* } /> */} + {/* Rewards Tab */} + {/* } /> */} - {/* Activity Tab */} - }> - } /> - } - /> - + {/* Activity Tab */} + }> + } /> + } + /> - - - + + + diff --git a/packages/widget/src/common/get-token-balances.ts b/packages/widget/src/common/get-token-balances.ts deleted file mode 100644 index f526b639..00000000 --- a/packages/widget/src/common/get-token-balances.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { QueryClient } from "@tanstack/react-query"; -import { EitherAsync, Right } from "purify-ts"; -import type { SKWallet } from "../domain/types/wallet"; -import type { DashboardYieldCategory } from "../domain/types/yields"; -import { - getDefaultTokens, - getYieldTypesForDashboardCategory, -} from "../hooks/api/use-default-tokens"; -import { getTokenBalancesScan } from "../hooks/api/use-token-balances-scan"; -import type { ApiClient } from "../providers/api/api-client"; -import type { SettingsProps } from "../providers/settings/types"; - -export const getTokenBalances = ({ - additionalAddresses, - address, - apiClient, - network, - selectedDashboardYieldCategory, - queryClient, - tokensForEnabledYieldsOnly, -}: { - additionalAddresses: SKWallet["additionalAddresses"]; - address: SKWallet["address"]; - apiClient: ApiClient; - queryClient: QueryClient; - network: SKWallet["network"]; - selectedDashboardYieldCategory?: DashboardYieldCategory | null; - tokensForEnabledYieldsOnly: SettingsProps["tokensForEnabledYieldsOnly"]; -}) => - EitherAsync.fromPromise(() => - Promise.all([ - getDefaultTokens({ - apiClient, - queryClient, - network: network ?? undefined, - enabledYieldsOnly: tokensForEnabledYieldsOnly, - yieldTypes: getYieldTypesForDashboardCategory( - selectedDashboardYieldCategory - ), - }), - EitherAsync.liftEither( - Right({ additionalAddresses, address, network }) - ).chain(async (params) => { - if (!params.address || !params.network) { - return Right([]); - } - - return getTokenBalancesScan({ - apiClient, - queryClient, - tokenBalanceScanDto: { - addresses: { - address: params.address, - additionalAddresses: params.additionalAddresses ?? undefined, - }, - network: params.network, - }, - }).mapLeft(() => new Error("could not get token balances scan")); - }), - ]).then(([defaultTokens, tokenBalances]) => - defaultTokens.chain((d) => - tokenBalances.map((b) => ({ - defaultTokens: d, - tokenBalancesScan: b, - })) - ) - ) - ).mapLeft(() => new Error("could not get tokens")); diff --git a/packages/widget/src/components/molecules/select-validator/index.tsx b/packages/widget/src/components/molecules/select-validator/index.tsx index 9b057f0d..bd8fc504 100644 --- a/packages/widget/src/components/molecules/select-validator/index.tsx +++ b/packages/widget/src/components/molecules/select-validator/index.tsx @@ -1,8 +1,8 @@ import type { PropsWithChildren } from "react"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import type { Validator, ValidatorKey } from "../../../domain/types/validators"; import type { Yield } from "../../../domain/types/yields"; -import type { ValidatorDto } from "../../../generated/api/yield"; import { Box } from "../../atoms/box"; import type { SelectModalProps } from "../../atoms/select-modal"; import { SelectModal } from "../../atoms/select-modal"; @@ -16,13 +16,13 @@ type SelectValidatorProps = PropsWithChildren< SelectModalProps, "isLoading" | "onClose" | "onOpen" | "state" | "trigger" > & { - selectedValidators: Set; - onItemClick: (item: ValidatorDto) => void; + selectedValidators: Set; + onItemClick: (item: Validator) => void; onViewMoreClick?: () => void; onLoadMore?: () => void; hasMore?: boolean; isLoadingMore?: boolean; - validators: ValidatorDto[]; + validators: Validator[]; selectedStake: Yield; multiSelect: boolean; } & ( @@ -71,7 +71,7 @@ export const SelectValidator = ({ const showExpandedValidators = multiSelect || hasRequestedMoreValidators; const data = useMemo<{ - tableData: ValidatorDto[]; + tableData: Validator[]; groupedItems: GroupedItem[]; groupCounts: number[]; }>(() => { @@ -106,11 +106,11 @@ export const SelectValidator = ({ }, { preferred: { - items: [] as ValidatorDto[], + items: [] as Validator[], label: t("details.validators_preferred"), }, other: { - items: [] as ValidatorDto[], + items: [] as Validator[], label: t("details.validators_other"), }, } diff --git a/packages/widget/src/components/molecules/select-validator/select-validator-list.tsx b/packages/widget/src/components/molecules/select-validator/select-validator-list.tsx index 2047dfbb..2d7fe9bc 100644 --- a/packages/widget/src/components/molecules/select-validator/select-validator-list.tsx +++ b/packages/widget/src/components/molecules/select-validator/select-validator-list.tsx @@ -1,8 +1,8 @@ import type { ComponentProps } from "react"; import { memo } from "react"; import { useTranslation } from "react-i18next"; +import type { Validator, ValidatorKey } from "../../../domain/types/validators"; import type { Yield } from "../../../domain/types/yields"; -import type { ValidatorDto } from "../../../generated/api/yield"; import { vars } from "../../../styles/theme/contract.css"; import { getRewardRateFormatted, @@ -31,7 +31,7 @@ import { validatorVirtuosoContainer, } from "./styles.css"; -export type GroupedItem = { items: ValidatorDto[]; label: string }; +export type GroupedItem = { items: Validator[]; label: string }; export const SelectValidatorList = ({ multiSelect, @@ -44,11 +44,11 @@ export const SelectValidatorList = ({ tableData, }: { multiSelect: boolean; - selectedValidators: Set; - onItemClick: (item: ValidatorDto) => void; + selectedValidators: Set; + onItemClick: (item: Validator) => void; onViewMoreClick: () => void; selectedStake: Yield; - tableData: ValidatorDto[]; + tableData: Validator[]; groupedItems: GroupedItem[]; groupCounts: number[]; }) => { @@ -100,7 +100,7 @@ export const SelectValidatorList = ({ const status = item.status; - const itemSelected = selectedValidators.has(item.address); + const itemSelected = selectedValidators.has(item.key); const _onItemClick: ComponentProps< typeof SelectModalItem @@ -115,7 +115,7 @@ export const SelectValidatorList = ({ @@ -135,7 +135,7 @@ export const SelectValidatorList = ({ justifyContent="center" alignItems="center" > - {selectedValidators.has(item.address) ? ( + {itemSelected ? ( ) : null} diff --git a/packages/widget/src/domain/types/action.ts b/packages/widget/src/domain/types/action.ts index 380597ae..119de2e2 100644 --- a/packages/widget/src/domain/types/action.ts +++ b/packages/widget/src/domain/types/action.ts @@ -17,8 +17,6 @@ export type TransactionStatus = TransactionDto["status"]; export type YieldCreateActionDto = YieldCreateActionDtoGenerated; export type YieldCreateManageActionDto = YieldCreateManageActionDtoGenerated; -export type YieldActionDto = ActionDto; -export type YieldTransactionDto = TransactionDto; type TransactionGasEstimate = { amount: string; gasLimit?: string; diff --git a/packages/widget/src/domain/types/positions.ts b/packages/widget/src/domain/types/positions.ts index 00a49368..5947ac6f 100644 --- a/packages/widget/src/domain/types/positions.ts +++ b/packages/widget/src/domain/types/positions.ts @@ -2,16 +2,26 @@ import BigNumber from "bignumber.js"; import type { BalanceDto, BalancesRequestDto, - ValidatorDto, YieldBalancesDto, BalanceType as YieldBalanceTypeGenerated, } from "../../generated/api/yield"; import { equalTokens } from ".."; import type { YieldRewardRateDto } from "./reward-rate"; import type { TokenDto } from "./tokens"; - -export type YieldBalanceDto = BalanceDto; -export type YieldBalancesByYieldDto = YieldBalancesDto; +import { + toValidator, + toValidators, + type Validator, + type ValidatorKey, +} from "./validators"; + +export type YieldBalanceDto = Omit & { + readonly validator?: Validator; + readonly validators?: ReadonlyArray; +}; +export type YieldBalancesByYieldDto = Omit & { + readonly balances: ReadonlyArray; +}; export type YieldBalancesRequestDto = BalancesRequestDto; export type YieldBalanceType = YieldBalanceTypeGenerated; @@ -21,14 +31,13 @@ export type PositionBalancesByType = Map< tokenPriceInUsd: BigNumber; })[] >; +export type PositionValidators = ReadonlyArray; export type PositionDetailsLabelType = "hasFrozenV1"; type BalanceType = "validators" | "default"; -export type BalanceDataKey = - | BalanceType - | `validator::${ValidatorDto["address"]}`; +export type BalanceDataKey = BalanceType | `validator::${ValidatorKey}`; export type PositionsData = Map< YieldBalancesByYieldDto["yieldId"], @@ -38,13 +47,30 @@ export type PositionsData = Map< balanceData: Map< BalanceDataKey, { balances: YieldBalanceDto[] } & ( - | { type: "validators"; validators: ReadonlyArray } + | { type: "validators"; validators: PositionValidators } | { type: "default" } ) >; } >; +export const toYieldBalance = (balance: BalanceDto): YieldBalanceDto => ({ + ...balance, + validator: balance.validator ? toValidator(balance.validator) : undefined, + validators: balance.validators ? toValidators(balance.validators) : undefined, +}); + +const toYieldBalancesByYield = ( + balancesByYield: YieldBalancesDto +): YieldBalancesByYieldDto => ({ + ...balancesByYield, + balances: balancesByYield.balances.map(toYieldBalance), +}); + +export const toYieldBalancesByYields = ( + balancesByYields: ReadonlyArray +): YieldBalancesByYieldDto[] => balancesByYields.map(toYieldBalancesByYield); + export const getPositionBalanceDataKey = ( balance: YieldBalanceDto ): BalanceDataKey => { @@ -52,8 +78,10 @@ export const getPositionBalanceDataKey = ( return "validators"; } - if (balance.validator?.address) { - return `validator::${balance.validator.address}` as BalanceDataKey; + const validator = balance.validator ?? balance.validators?.[0]; + + if (validator) { + return `validator::${validator.key}` as BalanceDataKey; } return "default"; diff --git a/packages/widget/src/domain/types/reward-rate.ts b/packages/widget/src/domain/types/reward-rate.ts index d2bbe3af..5f759187 100644 --- a/packages/widget/src/domain/types/reward-rate.ts +++ b/packages/widget/src/domain/types/reward-rate.ts @@ -1,16 +1,13 @@ -import type { - RewardDto, - ValidatorDto, - YieldDto, -} from "../../generated/api/yield"; +import type { RewardDto, YieldDto } from "../../generated/api/yield"; +import type { Validator, ValidatorKey } from "./validators"; type YieldRewardDto = RewardDto; export type YieldRewardRateDto = NonNullable; type YieldWithRewardRate = Pick; -type ValidatorRewardRateDto = NonNullable; +type ValidatorRewardRateDto = NonNullable; export type SelectedValidators = - | ReadonlyArray - | ReadonlyMap; + | ReadonlyArray + | ReadonlyMap; type RewardRateBreakdownKey = "native" | "protocol_incentive" | "campaign"; diff --git a/packages/widget/src/domain/types/stake.ts b/packages/widget/src/domain/types/stake.ts index 748cbd08..1f3ec9d2 100644 --- a/packages/widget/src/domain/types/stake.ts +++ b/packages/widget/src/domain/types/stake.ts @@ -1,13 +1,11 @@ import BigNumber from "bignumber.js"; import { List, Maybe } from "purify-ts"; -import type { ValidatorDto } from "../../generated/api/yield"; -import { tokenString } from ".."; import type { SupportedSKChains } from "./chains"; import { Networks } from "./chains/networks"; import type { InitParams } from "./init-params"; import type { PositionsData } from "./positions"; -import type { TokenBalanceScanResponseDto } from "./token-balance"; import type { TokenString } from "./tokens"; +import type { Validator, ValidatorKey } from "./validators"; import { getYieldActionArg, isBittensorStaking, @@ -15,81 +13,10 @@ import { type YieldBase, } from "./yields"; -const amountGreaterThanZero = (val: TokenBalanceScanResponseDto) => - new BigNumber(val.amount).isGreaterThan(0); - -const hasYields = (val: TokenBalanceScanResponseDto) => - !!val.availableYields.length; - -const hasYieldsAndAmount = (val: TokenBalanceScanResponseDto) => - hasYields(val) && amountGreaterThanZero(val); - export type PreferredTokenYieldsPerNetwork = { [Key in SupportedSKChains]?: Record; }; -export const getInitialToken = (args: { - initQueryParams: Maybe; - tokenBalances: ReadonlyArray; - defaultTokens: ReadonlyArray; - network: SupportedSKChains | null; - preferredTokenYieldsPerNetwork: PreferredTokenYieldsPerNetwork | null; -}) => - /** - * TB based on query params - */ - args.initQueryParams - .filter((val) => !!val.token) - .chain((val) => - List.find( - (t) => { - const tokenSymbolCompare = - val.token?.toLowerCase() === t.token.symbol.toLowerCase(); - - const tokenNetworkCompare = - val.network?.toLowerCase() === t.token.network.toLowerCase(); - - const tokenStringCompare = tokenString(t.token) === val.token; - - return ( - (tokenSymbolCompare && tokenNetworkCompare) || tokenStringCompare - ); - }, - [...args.tokenBalances, ...args.defaultTokens] - ) - ) - /** - * TB based on preferred token - */ - .altLazy(() => - Maybe.fromNullable(args.network) - .chain((n) => - Maybe.fromNullable(args.preferredTokenYieldsPerNetwork?.[n]) - ) - .altLazy(() => - Maybe.fromNullable(args.preferredTokenYieldsPerNetwork).chainNullable( - (v) => Object.values(v)[0] - ) - ) - .chain((preferredTokens) => - List.find( - (val) => !!preferredTokens[tokenString(val.token)], - [...args.tokenBalances, ...args.defaultTokens] - ) - ) - ) - /** - * TB based on first token with available yields and amount > 0 - */ - .altLazy(() => List.find(hasYieldsAndAmount, [...args.tokenBalances])) - /** - * TB based on first token with available yields - */ - .altLazy(() => List.find(hasYields, [...args.tokenBalances])) - .altLazy(() => List.find(hasYields, [...args.defaultTokens])) - .altLazy(() => List.head([...args.defaultTokens])) - .map((val) => val.token); - export const canBeInitialYield = (args: { initQueryParams: Maybe; yieldDto: YieldBase; @@ -127,7 +54,7 @@ const balanceValidForYield = ({ export const getInitSelectedValidators = (args: { initQueryParams: Maybe; - validators: ValidatorDto[]; + validators: Validator[]; }) => args.initQueryParams .chainNullable((params) => params.validator) @@ -140,8 +67,8 @@ export const getInitSelectedValidators = (args: { ) ) .altLazy(() => List.head(args.validators)) - .map((v) => new Map([[v.address, v]])) - .orDefault(new Map()); + .map((v) => new Map([[v.key, v]])) + .orDefault(new Map()); export const isForceMaxAmount = ( args: { minimum?: number | null; maximum?: number | null } | null | undefined @@ -151,9 +78,6 @@ const yieldsWithEnterMinBasedOnPosition = new Map>([ [Networks.Polkadot, new Set(["polkadot-dot-validator-staking"])], ]); -export const isNetworkWithEnterMinBasedOnPosition = (network: Networks) => - yieldsWithEnterMinBasedOnPosition.has(network); - const isYieldWithEnterMinBasedOnPosition = (yieldDto: YieldBase) => Maybe.fromNullable( yieldsWithEnterMinBasedOnPosition.get( diff --git a/packages/widget/src/domain/types/token-balance.ts b/packages/widget/src/domain/types/token-balance.ts index 72afea64..9bfd1dc8 100644 --- a/packages/widget/src/domain/types/token-balance.ts +++ b/packages/widget/src/domain/types/token-balance.ts @@ -1,15 +1,7 @@ import type { TokenBalanceScanDto as LegacyTokenBalanceScanDto, - TokenBalanceScanResponseDto as LegacyTokenBalanceScanResponseDto, YieldBalanceLabelDto as LegacyYieldBalanceLabelDto, } from "../../generated/api/legacy"; -import type { TokenDto } from "./tokens"; export type TokenBalanceScanDto = LegacyTokenBalanceScanDto; -export type TokenBalanceScanResponseDto = Omit< - LegacyTokenBalanceScanResponseDto, - "token" -> & { - readonly token: TokenDto; -}; export type YieldBalanceLabelDto = LegacyYieldBalanceLabelDto; diff --git a/packages/widget/src/domain/types/validators.ts b/packages/widget/src/domain/types/validators.ts new file mode 100644 index 00000000..fc58a3c6 --- /dev/null +++ b/packages/widget/src/domain/types/validators.ts @@ -0,0 +1,23 @@ +import type { ValidatorDto } from "../../generated/api/yield"; + +export type ValidatorKey = string; + +export type Validator = ValidatorDto & { + readonly key: ValidatorKey; +}; + +const getValidatorKey = ( + validator: Pick +): ValidatorKey => + validator.subnet?.id === undefined + ? validator.address + : `${validator.address}:${validator.subnet.id}`; + +export const toValidator = (validator: ValidatorDto): Validator => ({ + ...validator, + key: getValidatorKey(validator), +}); + +export const toValidators = ( + validators: ReadonlyArray +): Validator[] => validators.map(toValidator); diff --git a/packages/widget/src/domain/types/yields.ts b/packages/widget/src/domain/types/yields.ts index b4dcb18a..be242774 100644 --- a/packages/widget/src/domain/types/yields.ts +++ b/packages/widget/src/domain/types/yields.ts @@ -169,17 +169,17 @@ export const getDashboardYieldCategory = ( return null; }; -export const filterValidators = ({ +export const filterValidators = ({ validatorsConfig, validators, network, yieldId, }: { validatorsConfig: ValidatorsConfig; - validators: ValidatorDto[]; + validators: ReadonlyArray; network: Yield["token"]["network"]; yieldId?: Yield["id"]; -}) => { +}): T[] => { const valConfig = Maybe.fromNullable( validatorsConfig.get(network as SupportedSKChains) ) @@ -187,7 +187,7 @@ export const filterValidators = ({ .extractNullable(); const filtered = !valConfig - ? validators + ? [...validators] : (() => { const { allowed, @@ -206,10 +206,10 @@ export const filterValidators = ({ !!(mergePreferredWithDefault && v.preferred); if (preferredOnly) { - return isPreferred ? [{ ...v, preferred: true }] : []; + return isPreferred ? [{ ...v, preferred: true } as T] : []; } - return [{ ...v, preferred: isPreferred }]; + return [{ ...v, preferred: isPreferred } as T]; }); })(); diff --git a/packages/widget/src/hooks/api/use-activity-actions.ts b/packages/widget/src/hooks/api/use-activity-actions.ts index 3bfc3d5a..a889f939 100644 --- a/packages/widget/src/hooks/api/use-activity-actions.ts +++ b/packages/widget/src/hooks/api/use-activity-actions.ts @@ -10,11 +10,9 @@ import { getActionInputToken, getActionValidatorAddresses, } from "../../domain/types/action"; +import type { Validator } from "../../domain/types/validators"; import type { Yield } from "../../domain/types/yields"; -import type { - ActionsControllerGetActionsParams, - ValidatorDto, -} from "../../generated/api/yield"; +import type { ActionsControllerGetActionsParams } from "../../generated/api/yield"; import { type ActivityFilter, activityFilterCategories, @@ -39,7 +37,7 @@ const ACTIVITY_ACTION_STATUSES = [ type ActivityActionItem = { actionData: ActionDto; yieldData: Yield; - validatorsData: ValidatorDto[]; + validatorsData: Validator[]; }; type ActivityActionBaseItem = Omit; diff --git a/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts b/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts deleted file mode 100644 index 6ee2dfa8..00000000 --- a/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useQueries } from "@tanstack/react-query"; -import { useMemo } from "react"; -import type { TokenDto } from "../../domain/types/tokens"; -import { - type DashboardYieldCategory, - getApiYieldTypesForDashboardCategory, -} from "../../domain/types/yields"; -import { useApiClient } from "../../providers/api/api-client-provider"; -import { useSettings } from "../../providers/settings"; -import { useSKWallet } from "../../providers/sk-wallet"; -import { - DEFAULT_YIELD_SUMMARIES_PAGE_LIMIT, - fetchYieldSummariesPage, - getYieldSummariesQueryKey, - isVisibleYieldSummary, - type YieldSummariesParams, - type YieldSummary, -} from "./use-yield-summaries"; - -type DashboardCategoryInitialSelection = { - token: TokenDto; - yieldDto: YieldSummary; - yieldId: string; -}; - -const staleTime = 1000 * 60 * 2; - -/** - * Discovers available dashboard earn categories with one network-scoped, - * reward-rate-sorted probe per category (no dependency on the wallet's token - * holdings, no per-yield legacy hydration). The first visible summary of each - * probe seeds that category's initial token + yield selection. - */ -export const useDashboardYieldCatalog = ({ - enabled = true, -}: { - enabled?: boolean; -} = {}) => { - const { network, isConnecting } = useSKWallet(); - const apiClient = useApiClient(); - const { dashboardYieldCategoryOrder } = useSettings(); - - const probeEnabled = enabled && !isConnecting; - - const results = useQueries({ - queries: dashboardYieldCategoryOrder.map((category) => { - const params: YieldSummariesParams = { - ...(network ? { network: network } : {}), - types: getApiYieldTypesForDashboardCategory(category), - sort: "rewardRateDesc", - limit: DEFAULT_YIELD_SUMMARIES_PAGE_LIMIT, - }; - - return { - enabled: probeEnabled, - staleTime, - queryKey: getYieldSummariesQueryKey(params), - queryFn: ({ signal }: { signal: AbortSignal }) => - fetchYieldSummariesPage({ apiClient, params, signal }), - }; - }), - }); - - return useMemo(() => { - const initialSelectionByCategory = new Map< - DashboardYieldCategory, - DashboardCategoryInitialSelection - >(); - const availableCategories: DashboardYieldCategory[] = []; - - dashboardYieldCategoryOrder.forEach((category, index) => { - const firstVisible = (results[index]?.data ?? []).find( - isVisibleYieldSummary - ); - - if (!firstVisible) return; - - availableCategories.push(category); - initialSelectionByCategory.set(category, { - token: firstVisible.token, - yieldDto: firstVisible, - yieldId: firstVisible.id, - }); - }); - - return { - availableCategories, - initialSelectionByCategory, - isLoading: probeEnabled && results.some((result) => result.isLoading), - }; - }, [dashboardYieldCategoryOrder, results, probeEnabled]); -}; diff --git a/packages/widget/src/hooks/api/use-default-tokens.ts b/packages/widget/src/hooks/api/use-default-tokens.ts deleted file mode 100644 index d6f28f80..00000000 --- a/packages/widget/src/hooks/api/use-default-tokens.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { - type InfiniteData, - type QueryClient, - useInfiniteQuery, - useQuery, -} from "@tanstack/react-query"; -import { EitherAsync } from "purify-ts"; -import type { TokenBalanceScanResponseDto } from "../../domain/types/token-balance"; -import type { TokenDto } from "../../domain/types/tokens"; -import { - type DashboardYieldCategory, - getApiYieldTypesForDashboardCategory, -} from "../../domain/types/yields"; -import type { - TokenControllerGetTokensParams as LegacyTokenGetTokensParams, - TokenWithAvailableYieldsDto as LegacyTokenWithAvailableYieldsDto, -} from "../../generated/api/legacy"; -import type { - TokensControllerGetTokensParams as YieldTokenGetTokensParams, - TokenWithAvailableYieldsDto as YieldTokenWithAvailableYieldsDto, -} from "../../generated/api/yield"; -import type { ApiClient } from "../../providers/api/api-client"; -import { useApiClient } from "../../providers/api/api-client-provider"; -import { useSettings } from "../../providers/settings"; -import { useSKWallet } from "../../providers/sk-wallet"; - -const DEFAULT_TOKENS_PAGE_LIMIT = 100; -const DEFAULT_TOKENS_PAGE_CONCURRENCY = 5; - -type YieldTokenTypes = YieldTokenGetTokensParams["yieldTypes"]; -type DefaultTokensQueryParams = { - enabledYieldsOnly?: boolean; - network?: TokenDto["network"]; - yieldTypes?: YieldTokenTypes; -}; -type DefaultTokensPage = { - limit?: number; - nextOffset?: number; - offset?: number; - tokens: TokenBalanceScanResponseDto[]; - total?: number; -}; -type DefaultTokensPages = { - pages: DefaultTokensPage[]; - pageParams: number[]; -}; -type FetchDefaultTokensPageParams = DefaultTokensQueryParams & { - apiClient: ApiClient; - limit?: number; - offset?: number; - signal?: AbortSignal; -}; - -const noopFetchNextPage = () => undefined; - -const getTokenGetTokensQueryKey = (params?: DefaultTokensQueryParams) => - ["/v1/tokens", ...(params ? [params] : [])] as const; - -const getAllDefaultTokensQueryKey = (params?: DefaultTokensQueryParams) => - ["/v1/tokens/all-pages", ...(params ? [params] : [])] as const; - -const getNextOffset = ({ - limit, - offset, - total, -}: { - limit: number; - offset: number; - total: number; -}) => { - const nextOffset = offset + limit; - - return nextOffset < total ? nextOffset : undefined; -}; - -const shouldUseYieldTokensApi = ({ - enabledYieldsOnly, - yieldTypes, -}: Pick) => - !!enabledYieldsOnly || !!yieldTypes?.length; - -const toTokenBalanceScanResponse = ( - tokenWithYields: - | LegacyTokenWithAvailableYieldsDto - | YieldTokenWithAvailableYieldsDto -): TokenBalanceScanResponseDto => ({ - token: tokenWithYields.token, - availableYields: tokenWithYields.availableYields, - amount: "0", -}); - -export const getYieldTypesForDashboardCategory = ( - yieldCategory?: DashboardYieldCategory | null -): YieldTokenTypes => - yieldCategory - ? getApiYieldTypesForDashboardCategory(yieldCategory) - : undefined; - -export const fetchDefaultTokens = async ({ - apiClient, - enabledYieldsOnly, - limit = DEFAULT_TOKENS_PAGE_LIMIT, - network, - offset: firstOffset = 0, - signal, - yieldTypes, -}: FetchDefaultTokensPageParams): Promise => { - if (shouldUseYieldTokensApi({ enabledYieldsOnly, yieldTypes })) { - const yieldTokenParams = { - networks: network - ? [ - network as NonNullable< - YieldTokenGetTokensParams["networks"] - >[number], - ] - : undefined, - yieldTypes, - limit, - }; - - const fetchYieldTokensPage = async ( - offset: number - ): Promise => { - const page = await apiClient - .withOptions({ signal }) - .yield.TokensControllerGetTokens({ - params: { ...yieldTokenParams, offset }, - }); - - return { - limit: page.limit, - tokens: (page.items ?? []).map(toTokenBalanceScanResponse), - nextOffset: getNextOffset(page), - offset: page.offset, - total: page.total, - }; - }; - - const firstPage = await fetchYieldTokensPage(firstOffset); - - if (firstPage.nextOffset === undefined) { - return { pages: [firstPage], pageParams: [firstOffset] }; - } - - const remainingOffsets: number[] = []; - for ( - let offset = firstPage.offset! + firstPage.limit!; - offset < firstPage.total!; - offset += firstPage.limit! - ) { - remainingOffsets.push(offset); - } - - const pages = [firstPage]; - const pageParams = [firstOffset, ...remainingOffsets]; - - for ( - let i = 0; - i < remainingOffsets.length; - i += DEFAULT_TOKENS_PAGE_CONCURRENCY - ) { - const chunk = remainingOffsets.slice( - i, - i + DEFAULT_TOKENS_PAGE_CONCURRENCY - ); - const chunkPages = await Promise.all( - chunk.map((offset) => fetchYieldTokensPage(offset)) - ); - - pages.push(...chunkPages); - } - - return { pages, pageParams }; - } - - const tokens = await apiClient - .withOptions({ signal }) - .legacy.TokenControllerGetTokens({ - params: { - enabledYieldsOnly: enabledYieldsOnly || undefined, - network: network as LegacyTokenGetTokensParams["network"], - }, - }); - - return { - pages: [{ tokens: tokens.map(toTokenBalanceScanResponse) }], - pageParams: [firstOffset], - }; -}; - -export const useDefaultTokens = ({ - yieldCategory, -}: { - yieldCategory?: DashboardYieldCategory | null; -} = {}) => { - const { network } = useSKWallet(); - const { tokensForEnabledYieldsOnly } = useSettings(); - const apiClient = useApiClient(); - const queryParams: DefaultTokensQueryParams = { - enabledYieldsOnly: !!tokensForEnabledYieldsOnly, - network: network ?? undefined, - yieldTypes: getYieldTypesForDashboardCategory(yieldCategory), - }; - const shouldFetchAllPages = !!queryParams.yieldTypes?.length; - - const allPagesQuery = useQuery({ - queryKey: getAllDefaultTokensQueryKey(queryParams), - enabled: shouldFetchAllPages, - queryFn: async ({ signal }) => { - const data = await fetchDefaultTokens({ - ...queryParams, - apiClient, - signal, - }); - - return data.pages.flatMap((page) => page.tokens); - }, - staleTime: 1000 * 60 * 5, - }); - - const infiniteQuery = useInfiniteQuery({ - queryKey: getTokenGetTokensQueryKey(queryParams), - enabled: !shouldFetchAllPages, - initialPageParam: 0, - queryFn: async ({ pageParam, signal }) => { - if (shouldUseYieldTokensApi(queryParams)) { - const page = await apiClient - .withOptions({ signal }) - .yield.TokensControllerGetTokens({ - params: { - networks: queryParams.network - ? [ - queryParams.network as NonNullable< - YieldTokenGetTokensParams["networks"] - >[number], - ] - : undefined, - yieldTypes: queryParams.yieldTypes, - offset: pageParam, - limit: DEFAULT_TOKENS_PAGE_LIMIT, - }, - }); - - return { - limit: page.limit, - tokens: (page.items ?? []).map(toTokenBalanceScanResponse), - nextOffset: getNextOffset(page), - offset: page.offset, - total: page.total, - }; - } - - const tokens = await apiClient - .withOptions({ signal }) - .legacy.TokenControllerGetTokens({ - params: { - enabledYieldsOnly: queryParams.enabledYieldsOnly || undefined, - network: - queryParams.network as LegacyTokenGetTokensParams["network"], - }, - }); - - return { - tokens: tokens.map(toTokenBalanceScanResponse), - }; - }, - getNextPageParam: (lastPage) => lastPage.nextOffset, - select: (data) => data.pages.flatMap((page) => page.tokens), - staleTime: 1000 * 60 * 5, - }); - - if (shouldFetchAllPages) { - return { - ...allPagesQuery, - fetchNextPage: noopFetchNextPage, - hasNextPage: false, - isFetchingNextPage: false, - }; - } - - return infiniteQuery; -}; - -export const getDefaultTokens = ( - params: Omit & { - queryClient: QueryClient; - } -) => { - const queryParams: DefaultTokensQueryParams = { - enabledYieldsOnly: params.enabledYieldsOnly, - network: params.network, - yieldTypes: params.yieldTypes, - }; - - return EitherAsync(() => - params.queryClient.fetchQuery({ - queryKey: getAllDefaultTokensQueryKey(queryParams), - queryFn: async () => { - const data = await fetchDefaultTokens(params); - params.queryClient.setQueryData< - InfiniteData - >(getTokenGetTokensQueryKey(queryParams), data); - - return data.pages.flatMap((page) => page.tokens); - }, - staleTime: 1000 * 60 * 5, - }) - ).mapLeft((e) => { - console.log(e); - return new Error("could not get default tokens"); - }); -}; diff --git a/packages/widget/src/hooks/api/use-multi-yields.ts b/packages/widget/src/hooks/api/use-multi-yields.ts index a00b2fe5..9d8c9202 100644 --- a/packages/widget/src/hooks/api/use-multi-yields.ts +++ b/packages/widget/src/hooks/api/use-multi-yields.ts @@ -1,118 +1,27 @@ -import { hashKey, type QueryClient, useQuery } from "@tanstack/react-query"; -import { useSelector } from "@xstate/react"; -import { createStore } from "@xstate/store"; -import type BigNumber from "bignumber.js"; -import { EitherAsync, Maybe } from "purify-ts"; -import { useEffect, useMemo } from "react"; +import { type QueryClient, useQuery } from "@tanstack/react-query"; import { createSelector } from "reselect"; import { - defaultIfEmpty, EMPTY, filter, firstValueFrom, from, map, mergeMap, - Observable, - of, - repeat, - take, - tap, - timer, toArray, } from "rxjs"; -import { tokenString } from "../../domain"; -import { - isSupportedChain, - type SupportedSKChains, -} from "../../domain/types/chains"; -import type { InitParams } from "../../domain/types/init-params"; -import type { PositionsData } from "../../domain/types/positions"; -import { - canBeInitialYield, - type PreferredTokenYieldsPerNetwork, -} from "../../domain/types/stake"; +import { isSupportedChain } from "../../domain/types/chains"; import type { SKWallet } from "../../domain/types/wallet"; -import { - type DashboardYieldCategory, - getDashboardYieldCategory, - isNonZeroRewardRateYield, - type ValidatorsConfig, - type Yield, - type YieldBase, +import type { + ValidatorsConfig, + Yield, + YieldBase, } from "../../domain/types/yields"; import { useApiClient } from "../../providers/api/api-client-provider"; import { useSKQueryClient } from "../../providers/query-client"; import { useSKWallet } from "../../providers/sk-wallet"; import { useSavedRef } from "../use-saved-ref"; import { useValidatorsConfig } from "../use-validators-config"; -import { - getYieldOpportunities, - getYieldOpportunityFromSummary, -} from "./use-yield-opportunity/get-yield-opportunity"; -import { - fetchYieldSummariesWithProvidersByIds, - type YieldSummaryWithProvider, -} from "./use-yield-summaries"; - -const multiYieldsStore = createStore({ - context: { data: new Map>() }, - on: { - "yield-opportunity": ( - context, - event: { data: { key: string; yieldDto: YieldSummaryWithProvider } } - ) => { - const newMap = new Map(context.data); - const prev = newMap.get(event.data.key) ?? new Map(); - - prev.set(event.data.yieldDto.id, event.data.yieldDto); - newMap.set(event.data.key, prev); - - return { data: newMap }; - }, - }, -}); - -export const useStreamYieldSummaries = (yieldIds: ReadonlyArray) => { - const { network, isConnected, isLedgerLive } = useSKWallet(); - const apiClient = useApiClient(); - - const argsRef = useSavedRef({ - isLedgerLive, - queryClient: useSKQueryClient(), - apiClient, - network, - isConnected, - }); - - const hashedKey = useMemo(() => hashKey(yieldIds), [yieldIds]); - - const validatorsConfig = useValidatorsConfig(); - - useEffect(() => { - const sub = multipleYieldSummaries$({ - ...argsRef.current, - yieldIds, - validatorsConfig, - }) - .pipe(repeat({ delay: () => timer(1000 * 60 * 2) })) - .subscribe({ - next: (v) => - multiYieldsStore.send({ - type: "yield-opportunity", - data: { yieldDto: v, key: hashedKey }, - }), - }); - - return () => sub.unsubscribe(); - }, [argsRef, yieldIds, hashedKey, validatorsConfig]); - - return useSelector(multiYieldsStore, (state) => { - const map = state.context.data.get(hashedKey); - - return map ? Array.from(map.values()) : []; - }); -}; +import { getYieldOpportunities } from "./use-yield-opportunity/get-yield-opportunity"; export const useMultiYields = ( yieldIds: ReadonlyArray, @@ -149,22 +58,6 @@ export const useMultiYields = ( }); }; -export const getFirstEligibleYield = ( - params: Parameters[0] -) => - EitherAsync(() => - params.queryClient.fetchQuery({ - queryKey: getFirstEligibleYieldQueryKey({ - yieldIds: params.yieldIds, - dashboardYieldCategory: params.dashboardYieldCategory, - }), - queryFn: () => firstValueFrom(firstEligibleYield$(params)), - }) - ).mapLeft((e) => { - console.log(e); - return new Error("could not get first eligible yield"); - }); - const multipleYields$ = (args: { isLedgerLive: boolean; queryClient: QueryClient; @@ -200,119 +93,6 @@ const multipleYields$ = (args: { ) ); -const multipleYieldSummaries$ = (args: { - isLedgerLive: boolean; - queryClient: QueryClient; - apiClient: ReturnType; - isConnected: boolean; - network: SKWallet["network"]; - yieldIds: ReadonlyArray; - validatorsConfig: ValidatorsConfig; -}) => - args.yieldIds.length === 0 - ? EMPTY - : from( - fetchYieldSummariesWithProvidersByIds({ - apiClient: args.apiClient, - queryClient: args.queryClient, - yieldIds: args.yieldIds, - }) - ).pipe( - mergeMap((v) => from(v)), - filter( - (v): v is YieldSummaryWithProvider => - !!( - v && - defaultFiltered({ - data: [v], - isConnected: args.isConnected, - network: args.network, - isLedgerLive: args.isLedgerLive, - }).length > 0 - ) - ) - ); - -const firstEligibleYield$ = (args: { - isLedgerLive: boolean; - queryClient: QueryClient; - apiClient: ReturnType; - isConnected: boolean; - network: SKWallet["network"]; - yieldIds: ReadonlyArray; - dashboardYieldCategory?: DashboardYieldCategory | null; - initParams: InitParams; - positionsData: PositionsData; - tokenBalanceAmount: BigNumber; - validatorsConfig: ValidatorsConfig; - preferredTokenYieldsPerNetwork: PreferredTokenYieldsPerNetwork | null; -}) => { - let defaultYield: YieldSummaryWithProvider | null = null; - - const successStream = multipleYieldSummaries$(args).pipe( - filter( - (y) => - !args.dashboardYieldCategory || - getDashboardYieldCategory(y) === args.dashboardYieldCategory - ), - tap((v) => { - if (isNonZeroRewardRateYield(v) || !defaultYield) { - defaultYield = v; - } - }), - filter((y) => { - const preferredYieldId = Maybe.fromNullable( - args.preferredTokenYieldsPerNetwork?.[ - y.token.network as SupportedSKChains - ]?.[tokenString(y.token)] - ) - .altLazy(() => - Maybe.fromNullable(args.preferredTokenYieldsPerNetwork).chainNullable( - (v) => Object.values(v)[0][tokenString(y.token)] - ) - ) - .extractNullable(); - - if (preferredYieldId) { - return y.id === preferredYieldId || preferredYieldId === "*"; - } - - return canBeInitialYield({ - initQueryParams: Maybe.fromNullable(args.initParams), - yieldDto: y, - tokenBalanceAmount: args.tokenBalanceAmount, - positionsData: args.positionsData, - }); - }), - take(1), - defaultIfEmpty(null), - mergeMap((yieldSummary) => { - const selectedYield = yieldSummary ?? defaultYield; - - if (!selectedYield) { - return of(null); - } - - return from( - getYieldOpportunityFromSummary({ - isLedgerLive: args.isLedgerLive, - yieldDto: selectedYield, - queryClient: args.queryClient, - apiClient: args.apiClient, - }) - ).pipe(map((v) => (v.isRight() ? v.extract() : null))); - }) - ); - - return new Observable((subscriber) => { - successStream.subscribe({ - complete: () => subscriber.complete(), - next: (v) => subscriber.next(v), - error: (e) => subscriber.error(e), - }); - }); -}; - type SelectorInputData = { data: YieldBase[]; isConnected: boolean; @@ -342,26 +122,3 @@ const defaultFiltered = createSelector( return network === o.token.network && defaultFilter; }) ); - -const getFirstEligibleYieldQueryKey = ({ - yieldIds, - dashboardYieldCategory, -}: { - yieldIds: ReadonlyArray; - dashboardYieldCategory?: DashboardYieldCategory | null; -}) => ["first-eligible-yield", yieldIds, dashboardYieldCategory ?? null]; - -export const getCachedFirstEligibleYield = ({ - queryClient, - yieldIds, - dashboardYieldCategory, -}: { - queryClient: QueryClient; - yieldIds: ReadonlyArray; - dashboardYieldCategory?: DashboardYieldCategory | null; -}) => - Maybe.fromNullable( - queryClient.getQueryData( - getFirstEligibleYieldQueryKey({ yieldIds, dashboardYieldCategory }) - ) - ); diff --git a/packages/widget/src/hooks/api/use-token-balances-scan.ts b/packages/widget/src/hooks/api/use-token-balances-scan.ts index e1183999..9c686832 100644 --- a/packages/widget/src/hooks/api/use-token-balances-scan.ts +++ b/packages/widget/src/hooks/api/use-token-balances-scan.ts @@ -1,4 +1,3 @@ -import type { QueryClient } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; import { EitherAsync, Just, Maybe } from "purify-ts"; import { useCallback, useMemo } from "react"; @@ -59,19 +58,6 @@ export const useTokenBalancesScan = () => { }); }; -export const getTokenBalancesScan = ( - params: Parameters[0] & { queryClient: QueryClient } -) => - EitherAsync(() => - params.queryClient.fetchQuery({ - queryKey: getTokenTokenBalancesScanQueryKey(params.tokenBalanceScanDto), - queryFn: async () => (await queryFn(params)).unsafeCoerce(), - }) - ).mapLeft((e) => { - console.log(e); - return new Error("could not get multi yields"); - }); - const queryFn = ({ apiClient, tokenBalanceScanDto, 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 8ad83024..cd3b4beb 100644 --- a/packages/widget/src/hooks/api/use-yield-balances-scan.ts +++ b/packages/widget/src/hooks/api/use-yield-balances-scan.ts @@ -5,6 +5,7 @@ import type { YieldBalancesByYieldDto, YieldBalancesRequestDto, } from "../../domain/types/positions"; +import { toYieldBalancesByYields } from "../../domain/types/positions"; import { useApiClient } from "../../providers/api/api-client-provider"; import { useSKQueryClient } from "../../providers/query-client"; import { useSKWallet } from "../../providers/sk-wallet"; @@ -64,7 +65,7 @@ export const useYieldBalancesScan = (opts?: { payload: param.dto, }), select: (data) => { - const items = data.items as YieldBalancesByYieldDto[]; + const items = toYieldBalancesByYields(data.items); if (opts?.select) { return opts.select(items); 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 058614f7..4c335469 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 @@ -24,9 +24,6 @@ type MultiParams = Omit & { type ParamsWithQueryClient = Params & { queryClient: QueryClient; }; -type ParamsWithYieldDto = Omit & { - yieldDto: YieldBase; -}; type MultiParamsWithQueryClient = MultiParams & { queryClient: QueryClient; }; @@ -78,18 +75,6 @@ export const getYieldOpportunity = (params: ParamsWithQueryClient) => return new Error("Could not get yield opportunity"); }); -export const getYieldOpportunityFromSummary = (params: ParamsWithYieldDto) => - EitherAsync(() => - params.queryClient.fetchQuery({ - queryKey: getKey({ ...params, yieldId: params.yieldDto.id }), - staleTime, - queryFn: () => hydrateYieldSummaryQueryFn(params), - }) - ).mapLeft((e) => { - console.log(e); - return new Error("Could not get yield opportunity"); - }); - export const getYieldOpportunities = (params: MultiParamsWithQueryClient) => EitherAsync(() => params.queryClient.fetchQuery({ @@ -171,32 +156,6 @@ const fn = ({ }); }; -const hydrateYieldSummaryQueryFn = async ({ - yieldDto, - queryClient, - signal, - suppressRichErrors, - apiClient, -}: ParamsWithYieldDto & { - signal?: AbortSignal; -}) => { - const client = apiClient.withOptions({ signal, suppressRichErrors }); - const provider = - yieldDto.provider ?? - (await fetchYieldProvider({ - client, - providerId: yieldDto.providerId, - queryClient, - })); - - return applyYieldOverrides( - createYield({ - provider, - yieldDto, - }) - ); -}; - const multiFn = ({ queryClient, yieldIds, diff --git a/packages/widget/src/hooks/api/use-yield-summaries.ts b/packages/widget/src/hooks/api/use-yield-summaries.ts index eb697dd4..4c81102c 100644 --- a/packages/widget/src/hooks/api/use-yield-summaries.ts +++ b/packages/widget/src/hooks/api/use-yield-summaries.ts @@ -1,14 +1,10 @@ import type { QueryClient } from "@tanstack/react-query"; -import { isSupportedChain } from "../../domain/types/chains"; +import { chunksOf } from "effect/Array"; import { isEthenaUsdeStaking, - isNonZeroRewardRateYield, type YieldProviderDetails, } from "../../domain/types/yields"; -import type { - YieldDto, - YieldsControllerGetYieldsParams, -} from "../../generated/api/yield"; +import type { YieldDto } from "../../generated/api/yield"; import type { useApiClient } from "../../providers/api/api-client-provider"; import { fetchYieldProviders } from "./use-yield-providers"; @@ -18,38 +14,13 @@ import { fetchYieldProviders } from "./use-yield-providers"; * list rendering need (`token`, `rewardRate`, `status`, `metadata`, * `mechanics.type`, `providerId`). */ -export type YieldSummary = YieldDto; -export type YieldSummaryWithProvider = YieldSummary & { +type YieldSummary = YieldDto; +type YieldSummaryWithProvider = YieldSummary & { provider?: YieldProviderDetails; }; -export const DEFAULT_YIELD_SUMMARIES_PAGE_LIMIT = 50; const DEFAULT_YIELD_IDS_CHUNK_SIZE = 100; -export type YieldSummariesParams = Pick< - YieldsControllerGetYieldsParams, - "network" | "types" | "inputToken" | "sort" | "limit" ->; - -/** - * A summary is "visible" when it is enterable, on a supported chain, and has a - * non-zero reward rate (matching the dashboard catalog's display semantics). - */ -export const isVisibleYieldSummary = (summary: YieldSummary): boolean => - summary.status.enter && - isSupportedChain(summary.token.network) && - isNonZeroRewardRateYield(summary); - -export const getYieldSummariesQueryKey = ( - params: YieldSummariesParams & { allPages?: boolean } -) => ["yield-summaries", params]; - -type FetchArgs = { - apiClient: ReturnType; - params: YieldSummariesParams; - signal?: AbortSignal; -}; - type FetchByIdsArgs = { apiClient: ReturnType; chunkSize?: number; @@ -64,30 +35,6 @@ type FetchByIdsWithProvidersArgs = FetchByIdsArgs & { const unique = (items: ReadonlyArray) => [...new Set(items)]; -const chunks = (items: ReadonlyArray, chunkSize: number): T[][] => { - const result: T[][] = []; - - for (let index = 0; index < items.length; index += chunkSize) { - result.push(items.slice(index, index + chunkSize)); - } - - return result; -}; - -/** - * Fetch a single page of yield summaries. - */ -export const fetchYieldSummariesPage = async ({ - apiClient, - params, - signal, -}: FetchArgs): Promise => { - const client = apiClient.withOptions({ signal }); - const result = await client.yield.YieldsControllerGetYields({ params }); - - return [...(result.items ?? [])]; -}; - /** * Fetch yield summaries by ID without ever sending an unbounded `yieldIds` * query array. Results are ordered according to the first occurrence of each @@ -110,7 +57,7 @@ export const fetchYieldSummariesByIds = async ({ const normalizedChunkSize = Math.max(1, chunkSize); const summariesById = new Map(); - for (const chunk of chunks(ids, normalizedChunkSize)) { + for (const chunk of chunksOf(ids, normalizedChunkSize)) { const result = await client.yield.YieldsControllerGetYields({ params: { yieldIds: chunk, diff --git a/packages/widget/src/hooks/api/use-yield-validators.ts b/packages/widget/src/hooks/api/use-yield-validators.ts index 18d9fc8c..5c88c467 100644 --- a/packages/widget/src/hooks/api/use-yield-validators.ts +++ b/packages/widget/src/hooks/api/use-yield-validators.ts @@ -3,6 +3,11 @@ import { type QueryClient, useInfiniteQuery, } from "@tanstack/react-query"; +import { + toValidator, + toValidators, + type Validator, +} from "../../domain/types/validators"; import type { ValidatorsConfig } from "../../domain/types/yields"; import { filterValidators, type Yield } from "../../domain/types/yields"; import type { ValidatorDto } from "../../generated/api/yield"; @@ -31,7 +36,7 @@ type PageParam = | { type: "search"; offset: number; search: string }; type Page = { - validators: ValidatorDto[]; + validators: Validator[]; nextPageParam?: PageParam; }; @@ -56,7 +61,7 @@ const setYieldValidatorsQueryData = ({ }: { queryClient: QueryClient; yieldId: Yield["id"]; - validators: ReadonlyArray; + validators: ReadonlyArray; }) => { validators.forEach((validator) => { queryClient.setQueryData( @@ -78,12 +83,12 @@ export const getYieldValidatorsByAddresses = async ({ yieldId: Yield["id"]; addresses: ReadonlyArray; suppressRichErrors?: boolean; -}): Promise => { +}): Promise => { const uniqueAddresses = [...new Set(addresses)]; const validators = new Map( await Promise.all( uniqueAddresses.map( - async (address): Promise<[ValidatorDto["address"], ValidatorDto]> => { + async (address): Promise<[ValidatorDto["address"], Validator]> => { try { const validator = await queryClient.fetchQuery({ queryKey: getYieldValidatorQueryKey({ yieldId, address }), @@ -100,18 +105,21 @@ export const getYieldValidatorsByAddresses = async ({ }); const lowerCaseAddress = address.toLowerCase(); - return ( + const matchingValidator = validatorsPage.items?.find( (validator) => validator.address.toLowerCase() === lowerCaseAddress - ) ?? null - ); + ) ?? null; + + return matchingValidator + ? toValidator(matchingValidator) + : null; }, }); - return [address, validator ?? { address }]; + return [address, validator ?? toValidator({ address })]; } catch { - return [address, { address }]; + return [address, toValidator({ address })]; } } ) @@ -140,7 +148,7 @@ const getFilteredValidators = ({ validatorsConfig, yieldId, }: Pick & { - validators: ValidatorDto[]; + validators: Validator[]; }) => network ? filterValidators({ @@ -151,19 +159,19 @@ const getFilteredValidators = ({ }) : validators; -const deduplicateValidatorsByAddress = ( +const deduplicateValidatorsByKey = ( validators: ReadonlyArray ) => { - const seenAddresses = new Set(); + const seenKeys = new Set(); return validators.filter((validator) => { - const address = validator.address.toLowerCase(); + const key = toValidator(validator).key; - if (seenAddresses.has(address)) { + if (seenKeys.has(key)) { return false; } - seenAddresses.add(address); + seenKeys.add(key); return true; }); @@ -212,7 +220,7 @@ const fetchPagedValidators = async ({ fetchPage({ offset, name: search }), fetchPage({ offset, address: search }), ]); - const items = deduplicateValidatorsByAddress([ + const items = deduplicateValidatorsByKey([ ...(namePage.items ?? []), ...(addressPage.items ?? []), ]); @@ -227,15 +235,16 @@ const fetchPagedValidators = async ({ return fetchPage({ offset, preferred: false }); })(); const rawValidators = [...(rawPage.items ?? [])]; + const validatorOptions = toValidators(rawValidators); setYieldValidatorsQueryData({ queryClient, yieldId, - validators: rawValidators, + validators: validatorOptions, }); const validators = getFilteredValidators({ - validators: rawValidators, + validators: validatorOptions, network, validatorsConfig, yieldId, @@ -330,15 +339,16 @@ const fetchValidatorsPage = async ({ const rawValidators = [firstPage, ...remainingPages].flatMap( (page) => page.items ?? [] ); + const validatorOptions = toValidators(rawValidators); setYieldValidatorsQueryData({ queryClient, yieldId, - validators: [...rawValidators, ...(otherPage.items ?? [])], + validators: [...validatorOptions, ...toValidators(otherPage.items ?? [])], }); const validators = getFilteredValidators({ - validators: rawValidators, + validators: validatorOptions, network, validatorsConfig, yieldId, diff --git a/packages/widget/src/hooks/use-estimated-rewards.ts b/packages/widget/src/hooks/use-estimated-rewards.ts index 5adf51d1..391fd985 100644 --- a/packages/widget/src/hooks/use-estimated-rewards.ts +++ b/packages/widget/src/hooks/use-estimated-rewards.ts @@ -1,8 +1,8 @@ import BigNumber from "bignumber.js"; import { List, Maybe } from "purify-ts"; import { useMemo } from "react"; +import type { Validator, ValidatorKey } from "../domain/types/validators"; import { isBittensorStaking, type Yield } from "../domain/types/yields"; -import type { ValidatorDto } from "../generated/api/yield"; import { formatNumber } from "../utils"; import { getRewardRateFormatted } from "../utils/formatters"; import { useProvidersDetails } from "./use-provider-details"; @@ -15,7 +15,7 @@ export const useEstimatedRewards = ({ }: { selectedStake: Maybe; stakeAmount: BigNumber; - selectedValidators: Map; + selectedValidators: Map; selectedProviderYieldId: Maybe; }) => { const providersDetails = useProvidersDetails({ diff --git a/packages/widget/src/hooks/use-handle-deep-links.ts b/packages/widget/src/hooks/use-handle-deep-links.ts index 82b1ce27..caa9acc6 100644 --- a/packages/widget/src/hooks/use-handle-deep-links.ts +++ b/packages/widget/src/hooks/use-handle-deep-links.ts @@ -3,7 +3,7 @@ import { useEffect } from "react"; import { useNavigate } from "react-router"; import { usePendingActionDeepLink } from "../pages/details/earn-page/state/use-pending-action-deep-link"; import { useMountAnimation } from "../providers/mount-animation"; -import { usePendingActionStore } from "../providers/pending-action-store"; +import { useSetPendingActionRequest } from "../providers/pending-action-store"; import { useSKWallet } from "../providers/sk-wallet"; import { useInitQueryParams } from "./use-init-query-params"; import { useSavedRef } from "./use-saved-ref"; @@ -11,7 +11,7 @@ import { useSavedRef } from "./use-saved-ref"; export const useHandleDeepLinks = () => { const pendingActionDeepLinkCheck = usePendingActionDeepLink(); const navigateRef = useSavedRef(useNavigate()); - const pendignActionStore = usePendingActionStore(); + const setPendingActionRequest = useSetPendingActionRequest(); const initQueryParams = useInitQueryParams(); const { mountAnimationFinished } = useMountAnimation(); @@ -52,9 +52,9 @@ export const useHandleDeepLinks = () => { appReady && val.type === "review" ) .ifJust((val) => { - pendignActionStore.send({ - type: "initFlow", - data: { + setPendingActionRequest( + Maybe.of({ + actionDto: Maybe.empty(), requestDto: val.pendingActionDto.requestDto, addresses: { address: val.pendingActionDto.address, @@ -64,14 +64,14 @@ export const useHandleDeepLinks = () => { integrationData: val.pendingActionDto.integrationData, interactedToken: val.balance.token, pendingActionType: val.pendingActionDto.requestDto.action, - }, - }); + }) + ); navigateRef.current( `positions/${val.yieldOp.id}/${val.balanceId}/pending-action/review` ); }); }, [ - pendignActionStore, + setPendingActionRequest, pendingActionDeepLinkCheck.data, appReady, navigateRef, diff --git a/packages/widget/src/hooks/use-positions-data.ts b/packages/widget/src/hooks/use-positions-data.ts index fcffacce..0db549db 100644 --- a/packages/widget/src/hooks/use-positions-data.ts +++ b/packages/widget/src/hooks/use-positions-data.ts @@ -4,6 +4,7 @@ import { type BalanceDataKey, getPositionBalanceDataKey, type PositionsData, + type PositionValidators, type YieldBalanceDto, type YieldBalancesByYieldDto, } from "../domain/types/positions"; @@ -65,7 +66,7 @@ const positionsDataSelector = createSelector( { balances: YieldBalanceDto[] } & ( | { type: "validators"; - validators: NonNullable; + validators: PositionValidators; } | { type: "default" } ) diff --git a/packages/widget/src/hooks/use-provider-details.ts b/packages/widget/src/hooks/use-provider-details.ts index 999d042a..65aa4a61 100644 --- a/packages/widget/src/hooks/use-provider-details.ts +++ b/packages/widget/src/hooks/use-provider-details.ts @@ -1,11 +1,11 @@ import { List, Maybe } from "purify-ts"; import { useMemo } from "react"; +import type { Validator, ValidatorKey } from "../domain/types/validators"; import { getYieldProviderYieldIds, isYieldWithProviderOptions, type Yield, } from "../domain/types/yields"; -import type { ValidatorDto } from "../generated/api/yield"; import type { GetMaybeJust } from "../types/utils"; import { getRewardRateFormatted } from "../utils/formatters"; import { useMultiYields } from "./api/use-multi-yields"; @@ -17,12 +17,12 @@ type Res = Maybe<{ rewardRate: number | undefined; rewardType: string | undefined; address?: string; - stakedBalance?: ValidatorDto["tvl"]; - votingPower?: ValidatorDto["votingPower"]; - commission?: ValidatorDto["commission"]; - website?: ValidatorDto["website"]; - status?: ValidatorDto["status"]; - preferred?: ValidatorDto["preferred"]; + stakedBalance?: Validator["tvl"]; + votingPower?: Validator["votingPower"]; + commission?: Validator["commission"]; + website?: Validator["website"]; + status?: Validator["status"]; + preferred?: Validator["preferred"]; }>; export const getProviderDetails = ({ @@ -32,7 +32,7 @@ export const getProviderDetails = ({ selectedProviderYieldId, }: { integrationData: Maybe; - validator: Maybe; + validator: Maybe; yields: Maybe>; selectedProviderYieldId: Maybe; }): Res => { @@ -119,7 +119,7 @@ export const useProvidersDetails = ({ selectedProviderYieldId, }: { integrationData: Maybe; - validators: Maybe | Map>; + validators: Maybe | Map>; selectedProviderYieldId: Maybe; }) => { const yields = useMultiYields( diff --git a/packages/widget/src/pages-dashboard/activity/activity-details.page.tsx b/packages/widget/src/pages-dashboard/activity/activity-details.page.tsx index f8671356..d8cb240a 100644 --- a/packages/widget/src/pages-dashboard/activity/activity-details.page.tsx +++ b/packages/widget/src/pages-dashboard/activity/activity-details.page.tsx @@ -1,4 +1,3 @@ -import { useSelector } from "@xstate/store/react"; import { Box } from "../../components/atoms/box"; import { ActionStatus, type TransactionType } from "../../domain/types/action"; import { useActivityComplete } from "../../pages/complete/hooks/use-activity-complete.hook"; @@ -6,20 +5,14 @@ import { useComplete } from "../../pages/complete/hooks/use-complete.hook"; import { CompletePageComponent } from "../../pages/complete/pages/common.page"; import { CompleteCommonContextProvider } from "../../pages/complete/state"; import { ActionReviewPage } from "../../pages/review/pages/action-review.page"; -import { useActivityContext } from "../../providers/activity-provider"; +import { + useActivitySelectedAction, + useActivitySelectedYield, +} from "../../providers/activity-provider"; export const ActivityDetailsPage = () => { - const activityContext = useActivityContext(); - - const selectedAction = useSelector( - activityContext, - (state) => state.context.selectedAction - ).extractNullable(); - - const selectedYield = useSelector( - activityContext, - (state) => state.context.selectedYield - ).extractNullable(); + const selectedAction = useActivitySelectedAction().extractNullable(); + const selectedYield = useActivitySelectedYield().extractNullable(); if (!selectedYield || !selectedAction) { return null; diff --git a/packages/widget/src/pages-dashboard/activity/activity.page.tsx b/packages/widget/src/pages-dashboard/activity/activity.page.tsx index bf8e16ec..ed4818ff 100644 --- a/packages/widget/src/pages-dashboard/activity/activity.page.tsx +++ b/packages/widget/src/pages-dashboard/activity/activity.page.tsx @@ -1,5 +1,4 @@ import { useConnectModal } from "@stakekit/rainbowkit"; -import { useSelector } from "@xstate/store/react"; import { Maybe } from "purify-ts"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; @@ -17,7 +16,10 @@ import { } from "../../pages/details/activity-page/state/activity-page.context"; import type { ActionYieldDto } from "../../pages/details/activity-page/types"; import { FallbackContent } from "../../pages/details/positions-page/components/fallback-content"; -import { useActivityContext } from "../../providers/activity-provider"; +import { + useActivitySelectedAction, + useSetActivitySelection, +} from "../../providers/activity-provider"; import { useSKWallet } from "../../providers/sk-wallet"; import { container } from "./styles.css"; @@ -100,7 +102,7 @@ const ActivityPageComponent = () => { const _ActivityPage = () => { const value = useActivityPageContext(); - const activityStore = useActivityContext(); + const setActivitySelection = useSetActivitySelection(); const { openConnectModal } = useConnectModal(); const { isConnected, network } = useSKWallet(); @@ -111,14 +113,13 @@ const _ActivityPage = () => { data.actionData.status === ActionStatus.SUCCESS || data.actionData.status === ActionStatus.PROCESSING ) { - activityStore.send({ - type: "setSelectedAction", - data: Maybe.of({ + setActivitySelection( + Maybe.of({ selectedAction: data.actionData, selectedYield: data.yieldData, selectedValidators: data.validatorsData, - }), - }); + }) + ); } if ( @@ -126,40 +127,30 @@ const _ActivityPage = () => { data.actionData.status === ActionStatus.WAITING_FOR_NEXT || data.actionData.status === ActionStatus.FAILED ) { - activityStore.send({ - type: "setSelectedAction", - data: Maybe.of({ + setActivitySelection( + Maybe.of({ selectedAction: data.actionData, selectedYield: data.yieldData, selectedValidators: data.validatorsData, - }), - }); + }) + ); } return; }; - const selectedAction = useSelector( - activityStore, - (state) => state.context.selectedAction - ); + const selectedAction = useActivitySelectedAction(); // biome-ignore lint: false useEffect(() => { - activityStore.send({ - type: "setSelectedAction", - data: Maybe.empty(), - }); - }, [network, activityStore]); + setActivitySelection(Maybe.empty()); + }, [network, setActivitySelection]); useEffect(() => { if (!isConnected && selectedAction.isJust()) { - activityStore.send({ - type: "setSelectedAction", - data: Maybe.empty(), - }); + setActivitySelection(Maybe.empty()); } - }, [isConnected, selectedAction, activityStore]); + }, [isConnected, selectedAction, setActivitySelection]); return ( diff --git a/packages/widget/src/pages-dashboard/activity/index.tsx b/packages/widget/src/pages-dashboard/activity/index.tsx index 385f71a7..b74d4abb 100644 --- a/packages/widget/src/pages-dashboard/activity/index.tsx +++ b/packages/widget/src/pages-dashboard/activity/index.tsx @@ -1,10 +1,12 @@ -import { useSelector } from "@xstate/store/react"; import { Maybe } from "purify-ts"; import { Outlet, useNavigate } from "react-router"; import { Box } from "../../components/atoms/box"; import { CaretLeftIcon } from "../../components/atoms/icons/caret-left"; import { AnimationPage } from "../../navigation/containers/animation-page"; -import { useActivityContext } from "../../providers/activity-provider"; +import { + useActivitySelectedAction, + useSetActivitySelection, +} from "../../providers/activity-provider"; import { useSettings } from "../../providers/settings"; import { combineRecipeWithVariant } from "../../utils/styles"; import { ActivityPage } from "./activity.page"; @@ -13,17 +15,13 @@ import { activityDetailsContainer } from "./styles.css"; export const ActivityTabPage = () => { const { variant } = useSettings(); const navigate = useNavigate(); - const activityStore = useActivityContext(); - - const selectedAction = useSelector( - activityStore, - (state) => state.context.selectedAction - ); + const selectedAction = useActivitySelectedAction(); + const setActivitySelection = useSetActivitySelection(); const showDetails = selectedAction.isJust(); const onBack = () => { - activityStore.send({ type: "setSelectedAction", data: Maybe.empty() }); + setActivitySelection(Maybe.empty()); navigate("/activity"); }; diff --git a/packages/widget/src/pages-dashboard/overview/earn-details/components/provider-selection-card.tsx b/packages/widget/src/pages-dashboard/overview/earn-details/components/provider-selection-card.tsx index dc54b599..2273e3a6 100644 --- a/packages/widget/src/pages-dashboard/overview/earn-details/components/provider-selection-card.tsx +++ b/packages/widget/src/pages-dashboard/overview/earn-details/components/provider-selection-card.tsx @@ -7,12 +7,12 @@ import { XIcon } from "../../../../components/atoms/icons/x-icon"; import { Image } from "../../../../components/atoms/image"; import { Text } from "../../../../components/atoms/typography/text"; import { SelectValidator } from "../../../../components/molecules/select-validator"; +import type { Validator } from "../../../../domain/types/validators"; import { isYieldActionArgRequired, isYieldValidatorSelectionRequired, type Yield, } from "../../../../domain/types/yields"; -import type { ValidatorDto } from "../../../../generated/api/yield"; import { useSelectValidator } from "../../../../pages/details/earn-page/components/select-validator-section/use-select-validator"; import { useEarnPageContext } from "../../../../pages/details/earn-page/state/earn-page-context"; import { @@ -32,13 +32,13 @@ type ProviderDetailsItem = NonNullable< type ProviderCardItem = { key: string; - commission: ProviderDetailsItem["commission"] | ValidatorDto["commission"]; + commission: ProviderDetailsItem["commission"] | Validator["commission"]; logo: string | undefined; name: string; preferred: boolean | undefined; - stakedBalance: ProviderDetailsItem["stakedBalance"] | ValidatorDto["tvlRaw"]; - status: ProviderDetailsItem["status"] | ValidatorDto["status"]; - validator: ValidatorDto | undefined; + stakedBalance: ProviderDetailsItem["stakedBalance"] | Validator["tvlRaw"]; + status: ProviderDetailsItem["status"] | Validator["status"]; + validator: Validator | undefined; website: string | undefined; }; @@ -89,7 +89,7 @@ export const ProviderSelectionCard = () => { tokenSymbol={yieldDto.token.symbol} /> } - selectedValidators={new Set(selectedValidatorsArr.map((v) => v.address))} + selectedValidators={new Set(selectedValidatorsArr.map((v) => v.key))} multiSelect={multiSelect} selectedStake={yieldDto} onItemClick={onItemClick} @@ -115,7 +115,7 @@ const ProviderCardsTrigger = ({ }: { items: ProviderCardItem[]; multiSelect: boolean; - onRemoveValidator: (item: ValidatorDto) => void; + onRemoveValidator: (item: Validator) => void; tokenSymbol: string; }) => { const { t } = useTranslation(); @@ -227,7 +227,7 @@ const getProviderCardItems = ({ yieldDto, }: { providerDetailsArr: ProviderDetailsItem[]; - selectedValidatorsArr: ValidatorDto[]; + selectedValidatorsArr: Validator[]; yieldDto: Yield; }): ProviderCardItem[] => { if (selectedValidatorsArr.length) { @@ -236,7 +236,7 @@ const getProviderCardItems = ({ const name = validator.name ?? validator.address; return { - key: validator.address, + key: validator.key, commission: providerDetails?.commission ?? validator.commission, logo: providerDetails?.logo ?? validator.logoURI, name: providerDetails?.name ?? name, diff --git a/packages/widget/src/pages-dashboard/position-details/components/position-details-stake-actions.tsx b/packages/widget/src/pages-dashboard/position-details/components/position-details-stake-actions.tsx index 61f3134c..43f5c074 100644 --- a/packages/widget/src/pages-dashboard/position-details/components/position-details-stake-actions.tsx +++ b/packages/widget/src/pages-dashboard/position-details/components/position-details-stake-actions.tsx @@ -1,18 +1,30 @@ -import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Navigate } from "react-router"; import { Box } from "../../../components/atoms/box"; +import { selectTokenButton } from "../../../components/atoms/button/styles.css"; +import { ContentLoaderSquare } from "../../../components/atoms/content-loader"; +import { Dropdown } from "../../../components/atoms/dropdown"; +import { MaxButton } from "../../../components/atoms/max-button"; +import { NumberInput } from "../../../components/atoms/number-input"; import { Spinner } from "../../../components/atoms/spinner"; +import { TokenIcon } from "../../../components/atoms/token-icon"; import { Text } from "../../../components/atoms/typography/text"; +import * as AmountToggle from "../../../components/molecules/amount-toggle"; import { KycGateCard } from "../../../components/molecules/kyc-gate-card"; +import type { TronResourceType } from "../../../domain/types/tron"; +import { getYieldActionArg } from "../../../domain/types/yields"; import { useUnstakeOrPendingActionParams } from "../../../hooks/navigation/use-unstake-or-pending-action-params"; +import { MetaInfo } from "../../../pages/components/meta-info"; import { PageCtaButton } from "../../../pages/components/page-cta"; -import { ExtraArgsSelection } from "../../../pages/details/earn-page/components/extra-args-selection"; -import { Footer } from "../../../pages/details/earn-page/components/footer"; -import { SelectTokenSection } from "../../../pages/details/earn-page/components/select-token-section"; -import { useEarnPageContext } from "../../../pages/details/earn-page/state/earn-page-context"; -import { useEarnPageDispatch } from "../../../pages/details/earn-page/state/earn-page-state-context"; -import { usePositionDetails } from "../../../pages/position-details/hooks/use-position-details"; +import { + minMaxContainer, + priceTxt, + selectTokenBalance, + selectTokenSection, +} from "../../../pages/details/earn-page/components/select-token-section/styles.css"; +import { useSettings } from "../../../providers/settings"; +import { combineRecipeWithVariant } from "../../../utils/styles"; +import { usePositionDetailsStake } from "../hooks/use-position-details-stake"; import { PositionDetailsActionTabs } from "./position-details-action-tabs"; import { positionDetailsActionsHasContent, @@ -20,47 +32,270 @@ import { } from "./position-details-actions"; import { container } from "./styles.css"; -const StakeKycGateSection = () => { - const { kycGate, kycGateIsChecking, kycProviderName, onKycStatusRefresh } = - useEarnPageContext(); +type PositionDetailsStakeState = ReturnType; - if (kycGate.state === "pass" && !kycGateIsChecking) return null; +const StakeKycGateSection = ({ + stake, +}: { + stake: PositionDetailsStakeState; +}) => { + if (stake.kycGate.state === "pass" && !stake.kycGateIsChecking) return null; return ( ); }; -const PositionDetailsStakeStateInitializer = ({ - positionDetails, +const FixedToken = ({ stake }: { stake: PositionDetailsStakeState }) => { + const { variant } = useSettings(); + + return stake.selectedToken + .map((token) => { + return ( + + + {token.symbol} + + ); + }) + .extractNullable(); +}; + +const PositionDetailsStakeTokenSection = ({ + stake, }: { - positionDetails: ReturnType; + stake: PositionDetailsStakeState; }) => { - const dispatch = useEarnPageDispatch(); - const positionYield = positionDetails.integrationData.extractNullable(); + const { t } = useTranslation(); + const { variant } = useSettings(); - useEffect(() => { - if (!positionYield) { - return; - } + const isLoading = stake.appLoading; + const { + submitted, + errors: { + stakeAmountGreaterThanAvailableAmount, + stakeAmountGreaterThanMax, + stakeAmountLessThanMin, + stakeAmountIsZero, + }, + } = stake.validation; + const errorInput = + (submitted && stakeAmountIsZero) || + stakeAmountGreaterThanAvailableAmount || + stakeAmountGreaterThanMax || + stakeAmountLessThanMin; + const errorBalance = stakeAmountGreaterThanAvailableAmount; + const min = stake.stakeMinAmount + .map((value) => `${t("shared.min")} ${value} ${stake.symbol}`) + .extractNullable(); + const max = stake.stakeMaxAmount + .map((value) => `${t("shared.max")} ${value} ${stake.symbol}`) + .extractNullable(); + const minMax = min || max; - dispatch({ type: "positionDetails/stake/initialize", data: positionYield }); - }, [dispatch, positionYield]); + return isLoading ? ( + + + + ) : ( + + + + + + + + + + - return null; + {minMax ? ( + + + {min && max ? `${min} / ${max}` : (min ?? max)} + + + ) : null} + + + + + {stake.formattedPrice} + + + + + + + {stake.selectedTokenAvailableAmount + .map((value) => ( + + + {({ state }) => ( + + {state === "full" + ? value.fullFormattedAmount + : value.shortFormattedAmount} +  {value.symbol} {t("shared.available")} + + )} + + + )) + .extractNullable()} + + + + {!stake.isStakeTokenSameAsGasToken && ( + + )} + + + + ); +}; + +const PositionDetailsStakeFooter = ({ + stake, +}: { + stake: PositionDetailsStakeState; +}) => ( + +); + +const PositionDetailsStakeExtraArgs = ({ + stake, +}: { + stake: PositionDetailsStakeState; +}) => { + const { t } = useTranslation(); + + return stake.selectedStake + .chainNullable((selectedStake) => + getYieldActionArg(selectedStake, "enter", "tronResource") + ) + .map((tronResources) => { + const options = (tronResources.options ?? []).map((value) => ({ + label: value, + value: value as TronResourceType, + })); + const selectedOption = stake.tronResource + .map((value) => ({ value, label: value })) + .extract(); + const isError = + stake.validation.submitted && stake.validation.errors.tronResource; + + return ( + + + + {t("details.tron_resources.label")} + + + + stake.onTronResourceSelect(value)} + selectedOption={selectedOption} + placeholder={t("details.tron_resources.placeholder")} + isError={isError} + /> + + ); + }) + .extractNullable(); }; export const PositionDetailsStakeActions = () => { - const positionDetails = usePositionDetails(); + const stake = usePositionDetailsStake(); + const { positionDetails } = stake; const { plain } = useUnstakeOrPendingActionParams(); - const { cta } = useEarnPageContext(); const { t } = useTranslation(); if (positionDetails.isLoading) { @@ -117,20 +352,16 @@ export const PositionDetailsStakeActions = () => { canUnstake={canUnstake} /> - - - + -