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}
/>
-
-
-
+
-
+
-
+
-
+
-
+
);
};
diff --git a/packages/widget/src/pages-dashboard/position-details/hooks/use-position-details-stake.ts b/packages/widget/src/pages-dashboard/position-details/hooks/use-position-details-stake.ts
new file mode 100644
index 00000000..a2478468
--- /dev/null
+++ b/packages/widget/src/pages-dashboard/position-details/hooks/use-position-details-stake.ts
@@ -0,0 +1,440 @@
+import { useConnectModal } from "@stakekit/rainbowkit";
+import { useMutation } from "@tanstack/react-query";
+import BigNumber from "bignumber.js";
+import { List, Maybe } from "purify-ts";
+import { useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import type { NumberInputProps } from "../../../components/atoms/number-input";
+import {
+ equalTokens,
+ getTokenPriceInUSD,
+ stakeTokenSameAsGasToken,
+} from "../../../domain";
+import { getKycProviderName } from "../../../domain/types/kyc";
+import type {
+ BalanceDataKey,
+ PositionsData,
+} from "../../../domain/types/positions";
+import { getInitSelectedValidators } from "../../../domain/types/stake";
+import type { TronResourceType } from "../../../domain/types/tron";
+import type { Validator, ValidatorKey } from "../../../domain/types/validators";
+import {
+ getYieldActionArg,
+ isYieldValidatorSelectionRequired,
+ type Yield,
+} from "../../../domain/types/yields";
+import { useTokenBalancesScan } from "../../../hooks/api/use-token-balances-scan";
+import { useTokensPrices } from "../../../hooks/api/use-tokens-prices";
+import { useYieldKycGate } from "../../../hooks/api/use-yield-kyc-gate";
+import { useYieldValidators } from "../../../hooks/api/use-yield-validators";
+import { useNavigateWithScrollToTop } from "../../../hooks/navigation/use-navigate-with-scroll-to-top";
+import {
+ getPositionDetailsStakeReviewPath,
+ usePositionDetailsStakeMatch,
+} from "../../../hooks/navigation/use-position-details-stake-match";
+import { useTrackEvent } from "../../../hooks/tracking/use-track-event";
+import { useAddLedgerAccount } from "../../../hooks/use-add-ledger-account";
+import { useEstimatedRewards } from "../../../hooks/use-estimated-rewards";
+import { useMaxMinYieldAmount } from "../../../hooks/use-max-min-yield-amount";
+import { useSavedRef } from "../../../hooks/use-saved-ref";
+import { useYieldType } from "../../../hooks/use-yield-type";
+import type { PageCta } from "../../../pages/components/page-cta";
+import { useAmountValidation } from "../../../pages/details/earn-page/state/use-amount-validation";
+import { useStakeEnterRequestDto } from "../../../pages/details/earn-page/state/use-stake-enter-request-dto";
+import { usePositionDetails } from "../../../pages/position-details/hooks/use-position-details";
+import { useSetEnterStakeRequest } from "../../../providers/enter-stake-store";
+import { useSettings } from "../../../providers/settings";
+import { useSKWallet } from "../../../providers/sk-wallet";
+import { defaultFormattedNumber, formatNumber } from "../../../utils";
+import { usePositionDetailsStakeMachine } from "../state/stake-machine";
+
+const resolveProviderYieldId = (selectedStake: Maybe) =>
+ selectedStake
+ .chainNullable((stake) => getYieldActionArg(stake, "enter", "providerId"))
+ .filter((arg) => !!arg.required && !!arg.options?.length)
+ .chain((arg) => List.head(arg.options ?? []));
+
+const resolveTronResource = (selectedStake: Maybe) =>
+ selectedStake
+ .chainNullable((stake) => getYieldActionArg(stake, "enter", "tronResource"))
+ .filter((arg) => !!arg.required)
+ .map(() => "ENERGY" as TronResourceType);
+
+export const usePositionDetailsStake = () => {
+ const { t } = useTranslation();
+ const positionDetails = usePositionDetails();
+ const positionDetailsStakeMatch = usePositionDetailsStakeMatch();
+ const integrationId = positionDetailsStakeMatch?.params.integrationId ?? "";
+ const balanceId = positionDetailsStakeMatch?.params.balanceId ?? "";
+ const { intent, dispatch } = usePositionDetailsStakeMachine({
+ integrationId,
+ balanceId,
+ });
+
+ const selectedStake = positionDetails.integrationData;
+ const selectedToken = selectedStake.map((stake) => stake.token);
+ const selectedProviderYieldId = resolveProviderYieldId(selectedStake);
+ const tronResource = Maybe.fromNullable(intent.tronResource).altLazy(() =>
+ resolveTronResource(selectedStake)
+ );
+
+ const tokenBalancesScan = useTokenBalancesScan();
+ const availableAmount = useMemo(
+ () =>
+ selectedToken
+ .chain((token) =>
+ Maybe.fromNullable(
+ tokenBalancesScan.data?.find((balance) =>
+ equalTokens(balance.token, token)
+ )
+ )
+ )
+ .map((balance) => new BigNumber(balance.amount)),
+ [selectedToken, tokenBalancesScan.data]
+ );
+
+ const positionsData = useMemo(
+ () =>
+ selectedStake
+ .map((stake) => {
+ const balances = [
+ ...positionDetails.positionBalancesByType
+ .map((byType) => [...byType.values()].flat())
+ .orDefault([]),
+ ];
+
+ return new Map([
+ [
+ stake.id,
+ {
+ yieldId: stake.id,
+ rewardRate: stake.rewardRate,
+ balanceData: new Map([
+ [
+ "default" as BalanceDataKey,
+ { type: "default" as const, balances },
+ ],
+ ]),
+ },
+ ],
+ ]) as PositionsData;
+ })
+ .orDefault(new Map() as PositionsData),
+ [positionDetails.positionBalancesByType, selectedStake]
+ );
+
+ const {
+ maxIntegrationAmount,
+ minIntegrationAmount,
+ minEnterOrExitAmount,
+ maxEnterOrExitAmount,
+ isForceMax,
+ } = useMaxMinYieldAmount({
+ type: "enter",
+ yieldOpportunity: selectedStake,
+ availableAmount,
+ positionsData,
+ });
+
+ const rawStakeAmount = new BigNumber(intent.stakeAmount);
+ const stakeAmount =
+ intent.useMaxAmount || !rawStakeAmount.isZero()
+ ? rawStakeAmount
+ : minEnterOrExitAmount;
+
+ const selectedTokenAvailableAmount = useMemo(
+ () =>
+ availableAmount.map((amount) => ({
+ symbol: selectedToken.mapOrDefault((token) => token.symbol, ""),
+ shortFormattedAmount: defaultFormattedNumber(amount),
+ fullFormattedAmount: formatNumber(amount),
+ amount,
+ })),
+ [availableAmount, selectedToken]
+ );
+
+ const validatorsRequired = selectedStake
+ .map(isYieldValidatorSelectionRequired)
+ .orDefault(false);
+ const yieldValidators = useYieldValidators({
+ enabled: validatorsRequired,
+ yieldId:
+ selectedStake.map((stake) => stake.id).extractNullable() ?? undefined,
+ network:
+ selectedStake.map((stake) => stake.token.network).extractNullable() ??
+ undefined,
+ });
+ const selectedValidators = useMemo(() => {
+ if (!validatorsRequired) {
+ return new Map();
+ }
+
+ const validators = yieldValidators.data ?? [];
+ return getInitSelectedValidators({
+ initQueryParams: Maybe.empty(),
+ validators,
+ });
+ }, [validatorsRequired, yieldValidators.data]);
+
+ const estimatedRewards = useEstimatedRewards({
+ selectedStake,
+ stakeAmount,
+ selectedValidators,
+ selectedProviderYieldId,
+ });
+
+ const pricesState = useTokensPrices({
+ token: selectedToken,
+ yieldDto: selectedStake,
+ });
+
+ const formattedPrice = useMemo(
+ () =>
+ Maybe.fromRecord({
+ prices: Maybe.fromNullable(pricesState.data),
+ selectedStake,
+ selectedToken,
+ }).mapOrDefault(
+ (value) =>
+ `$${defaultFormattedNumber(
+ getTokenPriceInUSD({
+ baseToken: value.selectedStake.token,
+ amount: stakeAmount,
+ token: value.selectedToken,
+ prices: value.prices,
+ pricePerShare: null,
+ })
+ )}`,
+ ""
+ ),
+ [pricesState.data, selectedStake, selectedToken, stakeAmount]
+ );
+
+ const stakeEnterRequestDto = useStakeEnterRequestDto({
+ selectedProviderYieldId,
+ selectedStake,
+ selectedToken,
+ selectedValidators,
+ stakeAmount,
+ tronResource,
+ useMaxAmount: intent.useMaxAmount,
+ });
+ const yieldKycGate = useYieldKycGate({ yieldDto: selectedStake });
+ const kycGateIsBlocking = yieldKycGate.isGateBlocking;
+ const kycProviderName = selectedStake
+ .map(getKycProviderName)
+ .extractNullable();
+ const onKycStatusRefresh = () => yieldKycGate.refetch();
+ const { openConnectModal } = useConnectModal();
+ const navigate = useNavigateWithScrollToTop();
+ const setEnterStakeRequest = useSetEnterStakeRequest();
+ const { isConnected, isLedgerLiveAccountPlaceholder, chain } = useSKWallet();
+
+ const {
+ stakeAmountGreaterThanAvailableAmount,
+ stakeAmountGreaterThanMax,
+ stakeAmountLessThanMin,
+ stakeAmountIsZero,
+ } = useAmountValidation({
+ availableAmount,
+ stakeAmount,
+ maxEnterOrExitAmount,
+ minEnterOrExitAmount,
+ });
+
+ const onClickHandler = useMutation({
+ mutationFn: async () => {
+ if (validation.hasErrors) return;
+ if (stakeEnterRequestDto.isNothing()) return;
+
+ if (!isConnected) return openConnectModal?.();
+ if (kycGateIsBlocking) return;
+
+ Maybe.fromRecord({
+ selectedToken,
+ stakeEnterRequestDto,
+ }).ifJust((value) => {
+ setEnterStakeRequest(
+ Maybe.of({
+ actionDto: Maybe.empty(),
+ addresses: value.stakeEnterRequestDto.addresses,
+ requestDto: value.stakeEnterRequestDto.dto,
+ selectedToken: value.selectedToken,
+ gasFeeToken: value.stakeEnterRequestDto.gasFeeToken,
+ selectedStake: value.stakeEnterRequestDto.selectedStake,
+ selectedValidators: value.stakeEnterRequestDto.selectedValidators,
+ })
+ );
+ navigate(
+ getPositionDetailsStakeReviewPath({ balanceId, integrationId }) ??
+ "/review"
+ );
+ });
+ },
+ });
+
+ const validation = useMemo(() => {
+ const errors = {
+ tronResource: false,
+ stakeAmountGreaterThanAvailableAmount,
+ stakeAmountGreaterThanMax,
+ stakeAmountLessThanMin,
+ stakeAmountIsZero,
+ };
+
+ if (
+ isConnected &&
+ selectedStake
+ .chainNullable((stake) =>
+ getYieldActionArg(stake, "enter", "tronResource")
+ )
+ .filter((arg) => !!arg.required)
+ .isJust() &&
+ tronResource.isNothing()
+ ) {
+ errors.tronResource = true;
+ }
+
+ return {
+ submitted: onClickHandler.status !== "idle",
+ hasErrors: Object.values(errors).some(Boolean),
+ errors,
+ };
+ }, [
+ isConnected,
+ onClickHandler.status,
+ selectedStake,
+ stakeAmountGreaterThanAvailableAmount,
+ stakeAmountGreaterThanMax,
+ stakeAmountIsZero,
+ stakeAmountLessThanMin,
+ tronResource,
+ ]);
+
+ const trackEvent = useTrackEvent();
+ const onMaxClick = () => {
+ trackEvent("positionDetailsPageMaxClicked", {
+ yieldId: selectedStake.map((stake) => stake.id).extractNullable(),
+ });
+ dispatch({
+ type: "stakeAmount/max",
+ amount: maxEnterOrExitAmount.toString(10),
+ });
+ };
+ const onStakeAmountChange: NumberInputProps["onChange"] = (amount) =>
+ dispatch({ type: "stakeAmount/change", amount: amount.toString(10) });
+ const onTronResourceSelect = (value: TronResourceType) =>
+ dispatch({ type: "tronResource/select", tronResource: value });
+ const onClickRef = useSavedRef(onClickHandler.mutate);
+
+ const addLedgerAccount = useAddLedgerAccount();
+ const connectClickRef = useSavedRef(() => {
+ if (isLedgerLiveAccountPlaceholder && chain) {
+ trackEvent("addLedgerAccountClicked");
+ return addLedgerAccount.mutate(chain);
+ }
+
+ trackEvent("connectWalletClicked");
+ openConnectModal?.();
+ });
+
+ const { externalProviders } = useSettings();
+ const isFetching =
+ positionDetails.isLoading ||
+ tokenBalancesScan.isLoading ||
+ yieldValidators.isLoading ||
+ pricesState.isLoading;
+ const buttonCTAText = useYieldType(selectedStake).mapOrDefault(
+ (yieldType) => yieldType.cta,
+ ""
+ );
+ const buttonDisabled =
+ isConnected &&
+ (isFetching || stakeEnterRequestDto.isNothing() || kycGateIsBlocking);
+ const appLoading = positionDetails.isLoading || selectedStake.isNothing();
+ const cta = useMemo(
+ () =>
+ isConnected && !isLedgerLiveAccountPlaceholder
+ ? {
+ disabled: buttonDisabled,
+ isLoading: !buttonCTAText || isFetching || yieldKycGate.isLoading,
+ onClick: () => onClickRef.current(),
+ label: buttonCTAText,
+ }
+ : externalProviders
+ ? null
+ : {
+ disabled: appLoading,
+ isLoading: appLoading,
+ label: t(
+ isLedgerLiveAccountPlaceholder
+ ? "init.ledger_add_account"
+ : "init.connect_wallet"
+ ),
+ onClick: () => connectClickRef.current(),
+ },
+ [
+ appLoading,
+ buttonCTAText,
+ buttonDisabled,
+ connectClickRef,
+ externalProviders,
+ isConnected,
+ isFetching,
+ isLedgerLiveAccountPlaceholder,
+ onClickRef,
+ t,
+ yieldKycGate.isLoading,
+ ]
+ );
+
+ const stakeMaxAmount = selectedStake
+ .filter(() => maxIntegrationAmount.isJust() && !isForceMax)
+ .map(() => maxEnterOrExitAmount.toNumber());
+ const stakeMinAmount = selectedStake
+ .filter(() => minIntegrationAmount.isJust() && !isForceMax)
+ .map(() => minEnterOrExitAmount.toNumber())
+ .filter((value) => new BigNumber(value).isGreaterThan(0));
+ const isStakeTokenSameAsGasToken = Maybe.fromRecord({
+ selectedStake,
+ selectedToken,
+ }).mapOrDefault(
+ (value) =>
+ stakeTokenSameAsGasToken({
+ stakeToken: value.selectedToken,
+ yieldDto: value.selectedStake,
+ }),
+ false
+ );
+
+ return {
+ appLoading,
+ cta,
+ estimatedRewards,
+ footerIsLoading: isFetching,
+ formattedPrice,
+ isFetching,
+ isStakeTokenSameAsGasToken,
+ kycGate: yieldKycGate.gate,
+ kycGateIsChecking:
+ yieldKycGate.isLoading ||
+ yieldKycGate.isFetching ||
+ yieldKycGate.isRefetching,
+ kycProviderName,
+ onKycStatusRefresh,
+ onMaxClick,
+ onStakeAmountChange,
+ onTronResourceSelect,
+ positionDetails,
+ selectedStake,
+ selectedToken,
+ selectedTokenAvailableAmount,
+ selectedValidators,
+ stakeAmount,
+ stakeMaxAmount,
+ stakeMinAmount,
+ symbol: selectedToken.mapOrDefault((token) => token.symbol, ""),
+ tronResource,
+ validation,
+ };
+};
diff --git a/packages/widget/src/pages-dashboard/position-details/position-details-model.tsx b/packages/widget/src/pages-dashboard/position-details/position-details-model.tsx
index 8895d8ee..c43879ac 100644
--- a/packages/widget/src/pages-dashboard/position-details/position-details-model.tsx
+++ b/packages/widget/src/pages-dashboard/position-details/position-details-model.tsx
@@ -86,7 +86,7 @@ type DashboardPositionDetailsModel = {
statusSummary: DashboardPositionStatusSummary;
};
-export type DashboardPositionPendingAction = {
+type DashboardPositionPendingAction = {
amount: BigNumber | null;
formattedAmount: string;
pendingActionDto: YieldPendingActionDto;
diff --git a/packages/widget/src/pages-dashboard/position-details/state/stake-machine.ts b/packages/widget/src/pages-dashboard/position-details/state/stake-machine.ts
new file mode 100644
index 00000000..3d607567
--- /dev/null
+++ b/packages/widget/src/pages-dashboard/position-details/state/stake-machine.ts
@@ -0,0 +1,88 @@
+import { useAtom } from "@effect/atom-react";
+import { Data } from "effect";
+import * as Atom from "effect/unstable/reactivity/Atom";
+import type { TronResourceType } from "../../../domain/types/tron";
+
+type PositionDetailsStakeEntryParams = {
+ integrationId: string;
+ balanceId: string;
+};
+
+class PositionDetailsStakeEntryKey extends Data.Class {}
+
+type PositionDetailsStakeIntent = {
+ stakeAmount: string;
+ tronResource: TronResourceType | null;
+ useMaxAmount: boolean;
+};
+
+type PositionDetailsStakeAction =
+ | {
+ type: "stakeAmount/change";
+ amount: string;
+ }
+ | {
+ type: "stakeAmount/max";
+ amount: string;
+ }
+ | {
+ type: "tronResource/select";
+ tronResource: TronResourceType;
+ };
+
+const makeDefaultIntent = (): PositionDetailsStakeIntent => ({
+ stakeAmount: "0",
+ tronResource: null,
+ useMaxAmount: false,
+});
+
+const positionDetailsStakeAtom = Atom.family(
+ (_entry: PositionDetailsStakeEntryKey) => {
+ const intentAtom = Atom.make(
+ makeDefaultIntent()
+ );
+
+ return Atom.writable<
+ PositionDetailsStakeIntent,
+ PositionDetailsStakeAction
+ >(
+ (ctx) => ctx.get(intentAtom),
+ (ctx, action) => {
+ const intent = ctx.get(intentAtom);
+
+ switch (action.type) {
+ case "stakeAmount/change":
+ ctx.set(intentAtom, {
+ ...intent,
+ stakeAmount: action.amount,
+ useMaxAmount: false,
+ });
+ return;
+ case "stakeAmount/max":
+ ctx.set(intentAtom, {
+ ...intent,
+ stakeAmount: action.amount,
+ useMaxAmount: true,
+ });
+ return;
+ case "tronResource/select":
+ ctx.set(intentAtom, {
+ ...intent,
+ tronResource: action.tronResource,
+ });
+ return;
+ }
+ }
+ );
+ }
+);
+
+export const usePositionDetailsStakeMachine = (
+ entry: PositionDetailsStakeEntryParams
+) => {
+ const [intent, dispatch] = useAtom(
+ positionDetailsStakeAtom(new PositionDetailsStakeEntryKey(entry))
+ );
+
+ return { intent, dispatch };
+};
diff --git a/packages/widget/src/pages/complete/hooks/use-activity-complete.hook.ts b/packages/widget/src/pages/complete/hooks/use-activity-complete.hook.ts
index e1152e51..19837dd7 100644
--- a/packages/widget/src/pages/complete/hooks/use-activity-complete.hook.ts
+++ b/packages/widget/src/pages/complete/hooks/use-activity-complete.hook.ts
@@ -1,4 +1,3 @@
-import { useSelector } from "@xstate/store/react";
import { Maybe } from "purify-ts";
import { useMemo } from "react";
import { getActionInputToken } from "../../../domain/types/action";
@@ -6,18 +5,17 @@ import type { TokenDto } from "../../../domain/types/tokens";
import { useTrackPage } from "../../../hooks/tracking/use-track-page";
import { useProvidersDetails } from "../../../hooks/use-provider-details";
import { useYieldType } from "../../../hooks/use-yield-type";
-import { useActivityContext } from "../../../providers/activity-provider";
+import {
+ useActivitySelectedAction,
+ useActivitySelectedValidators,
+ useActivitySelectedYield,
+} from "../../../providers/activity-provider";
import { defaultFormattedNumber } from "../../../utils";
export const useActivityComplete = () => {
useTrackPage("activityComplete");
- const activityContext = useActivityContext();
-
- const selectedAction = useSelector(
- activityContext,
- (state) => state.context.selectedAction
- ).unsafeCoerce();
+ const selectedAction = useActivitySelectedAction().unsafeCoerce();
const amount = useMemo(
() =>
@@ -27,15 +25,8 @@ export const useActivityComplete = () => {
[selectedAction]
);
- const selectedYield = useSelector(
- activityContext,
- (state) => state.context.selectedYield
- );
-
- const selectedValidators = useSelector(
- activityContext,
- (state) => state.context.selectedValidators
- );
+ const selectedYield = useActivitySelectedYield();
+ const selectedValidators = useActivitySelectedValidators();
const yieldType = useYieldType(selectedYield).map((v) => v.type);
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 fcd9bea7..6a375b53 100644
--- a/packages/widget/src/pages/complete/pages/pending-complete.page.tsx
+++ b/packages/widget/src/pages/complete/pages/pending-complete.page.tsx
@@ -1,4 +1,3 @@
-import { useSelector } from "@xstate/store/react";
import BigNumber from "bignumber.js";
import { Maybe } from "purify-ts";
import { useMemo } from "react";
@@ -7,7 +6,7 @@ import { useTrackPage } from "../../../hooks/tracking/use-track-page";
import { usePositionBalances } from "../../../hooks/use-position-balances";
import { useProvidersDetails } from "../../../hooks/use-provider-details";
import { useYieldType } from "../../../hooks/use-yield-type";
-import { usePendingActionStore } from "../../../providers/pending-action-store";
+import { usePendingActionRequest } from "../../../providers/pending-action-store";
import { defaultFormattedNumber } from "../../../utils";
import { CompletePage } from "./common.page";
@@ -19,10 +18,7 @@ export const PendingCompletePage = () => {
integrationId: plain.integrationId,
});
- const pendingRequest = useSelector(
- usePendingActionStore(),
- (state) => state.context.data
- ).unsafeCoerce();
+ const pendingRequest = usePendingActionRequest().unsafeCoerce();
const integrationData = useMemo(
() => Maybe.of(pendingRequest.integrationData),
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 65aa69f5..8701eafa 100644
--- a/packages/widget/src/pages/complete/pages/stake-complete.page.tsx
+++ b/packages/widget/src/pages/complete/pages/stake-complete.page.tsx
@@ -1,21 +1,17 @@
-import { useSelector } from "@xstate/store/react";
import BigNumber from "bignumber.js";
import { Maybe } from "purify-ts";
import { useMemo } from "react";
import { useTrackPage } from "../../../hooks/tracking/use-track-page";
import { useProvidersDetails } from "../../../hooks/use-provider-details";
import { useYieldType } from "../../../hooks/use-yield-type";
-import { useEnterStakeStore } from "../../../providers/enter-stake-store";
+import { useEnterStakeRequest } from "../../../providers/enter-stake-store";
import { defaultFormattedNumber } from "../../../utils";
import { CompletePage } from "./common.page";
export const StakeCompletePage = () => {
useTrackPage("stakeComplete");
- const enterRequest = useSelector(
- useEnterStakeStore(),
- (state) => state.context.data
- ).unsafeCoerce();
+ const enterRequest = useEnterStakeRequest().unsafeCoerce();
const selectedStake = useMemo(
() => Maybe.of(enterRequest.selectedStake),
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 b0d61797..5f5a16e0 100644
--- a/packages/widget/src/pages/complete/pages/unstake-complete.page.tsx
+++ b/packages/widget/src/pages/complete/pages/unstake-complete.page.tsx
@@ -1,4 +1,3 @@
-import { useSelector } from "@xstate/store/react";
import { Maybe } from "purify-ts";
import { useMemo } from "react";
import { useUnstakeOrPendingActionParams } from "../../../hooks/navigation/use-unstake-or-pending-action-params";
@@ -6,7 +5,7 @@ import { useTrackPage } from "../../../hooks/tracking/use-track-page";
import { usePositionBalances } from "../../../hooks/use-position-balances";
import { useProvidersDetails } from "../../../hooks/use-provider-details";
import { useYieldType } from "../../../hooks/use-yield-type";
-import { useExitStakeStore } from "../../../providers/exit-stake-store";
+import { useExitStakeRequest } from "../../../providers/exit-stake-store";
import { defaultFormattedNumber } from "../../../utils";
import { CompletePage } from "./common.page";
@@ -17,10 +16,7 @@ export const UnstakeCompletePage = () => {
integrationId: plain.integrationId,
});
- const exitRequest = useSelector(
- useExitStakeStore(),
- (state) => state.context.data
- ).unsafeCoerce();
+ const exitRequest = useExitStakeRequest().unsafeCoerce();
const integrationData = useMemo(
() => Maybe.of(exitRequest.integrationData),
diff --git a/packages/widget/src/pages/components/meta-info/index.tsx b/packages/widget/src/pages/components/meta-info/index.tsx
index 9578fe2a..3cbe5786 100644
--- a/packages/widget/src/pages/components/meta-info/index.tsx
+++ b/packages/widget/src/pages/components/meta-info/index.tsx
@@ -9,8 +9,8 @@ import { InfoIcon } from "../../../components/atoms/icons/info";
import type { TextVariants } from "../../../components/atoms/typography/styles.css";
import { Text } from "../../../components/atoms/typography/text";
import type { TokenDto } from "../../../domain/types/tokens";
+import type { Validator, ValidatorKey } from "../../../domain/types/validators";
import type { Yield } from "../../../domain/types/yields";
-import type { ValidatorDto } from "../../../generated/api/yield";
import { useYieldMetaInfo } from "../../../hooks/use-yield-meta-info";
import { dotContainer, dotText } from "./styles.css";
@@ -19,7 +19,7 @@ type MetaInfoTextSize = NonNullable["size"]>;
type Props = {
isLoading?: boolean;
selectedStake: Maybe;
- selectedValidators: Map;
+ selectedValidators: Map;
selectedToken: Maybe;
textSize?: MetaInfoTextSize;
};
diff --git a/packages/widget/src/pages/details/activity-page/state/activity-page.context.tsx b/packages/widget/src/pages/details/activity-page/state/activity-page.context.tsx
index 3e2c63ff..3d6c3f91 100644
--- a/packages/widget/src/pages/details/activity-page/state/activity-page.context.tsx
+++ b/packages/widget/src/pages/details/activity-page/state/activity-page.context.tsx
@@ -11,7 +11,7 @@ import {
useActivityFilterOptions,
usePrefetchActivityActionFilters,
} from "../../../../hooks/api/use-activity-actions";
-import { useActivityContext } from "../../../../providers/activity-provider";
+import { useSetActivitySelection } from "../../../../providers/activity-provider";
import { useSKWallet } from "../../../../providers/sk-wallet";
import { useActivityFilters } from "../hooks/use-activity-filters";
import type { ActionYieldDto } from "../types";
@@ -24,7 +24,7 @@ export const ActivityPageContext = createContext<
export const ActivityPageContextProvider = ({
children,
}: PropsWithChildren) => {
- const activityStore = useActivityContext();
+ const setActivitySelection = useSetActivitySelection();
const { openConnectModal } = useConnectModal();
const { isConnected } = useSKWallet();
const navigate = useNavigate();
@@ -36,14 +36,13 @@ export const ActivityPageContextProvider = ({
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,
- }),
- });
+ })
+ );
const urls = data.actionData.transactions
.map((val) => ({ type: val.type, url: val.explorerUrl }))
@@ -70,14 +69,13 @@ export const ActivityPageContextProvider = ({
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 navigate("/activity/review");
}
diff --git a/packages/widget/src/pages/details/activity-page/types.ts b/packages/widget/src/pages/details/activity-page/types.ts
index 42882de5..1bea2417 100644
--- a/packages/widget/src/pages/details/activity-page/types.ts
+++ b/packages/widget/src/pages/details/activity-page/types.ts
@@ -1,9 +1,9 @@
import type { ActionDto } from "../../../domain/types/action";
+import type { Validator } from "../../../domain/types/validators";
import type { Yield } from "../../../domain/types/yields";
-import type { ValidatorDto } from "../../../generated/api/yield";
export type ActionYieldDto = {
actionData: ActionDto;
yieldData: Yield;
- validatorsData: ValidatorDto[];
+ validatorsData: Validator[];
};
diff --git a/packages/widget/src/pages/details/earn-page/components/select-provider/index.tsx b/packages/widget/src/pages/details/earn-page/components/select-provider/index.tsx
index dfc5fe1b..4d4a31e3 100644
--- a/packages/widget/src/pages/details/earn-page/components/select-provider/index.tsx
+++ b/packages/widget/src/pages/details/earn-page/components/select-provider/index.tsx
@@ -14,10 +14,6 @@ import {
import { useMultiYields } from "../../../../../hooks/api/use-multi-yields";
import { formatCompactUsd } from "../../../../../utils/formatters";
import { useEarnPageContext } from "../../state/earn-page-context";
-import {
- useEarnPageDispatch,
- useEarnPageState,
-} from "../../state/earn-page-state-context";
import {
overflowEllipsis,
selectorSummaryCard,
@@ -45,9 +41,12 @@ const getProviderTvl = (tvlUsd: unknown) => {
};
export const SelectProvider = () => {
- const { selectedStake, selectedProviderYieldId } = useEarnPageState();
- const { appLoading } = useEarnPageContext();
- const dispatch = useEarnPageDispatch();
+ const {
+ appLoading,
+ onProviderYieldIdSelect,
+ selectedProviderYieldId,
+ selectedStake,
+ } = useEarnPageContext();
const { t } = useTranslation();
@@ -83,9 +82,7 @@ export const SelectProvider = () => {
providerSelection
.map((val) => (
- dispatch({ type: "providerYieldId/select", data: yieldDto.id })
- }
+ onItemClick={(yieldDto) => onProviderYieldIdSelect(yieldDto.id)}
providerYieldIds={val.providerYieldIdOptions}
selectedYieldId={val.selectedProviderYield.id}
trigger={
diff --git a/packages/widget/src/pages/details/earn-page/components/select-token-section/select-token-list-item.tsx b/packages/widget/src/pages/details/earn-page/components/select-token-section/select-token-list-item.tsx
index c57e6e3e..66b13d86 100644
--- a/packages/widget/src/pages/details/earn-page/components/select-token-section/select-token-list-item.tsx
+++ b/packages/widget/src/pages/details/earn-page/components/select-token-section/select-token-list-item.tsx
@@ -8,15 +8,15 @@ import {
} from "../../../../../components/atoms/select-modal";
import { TokenIcon } from "../../../../../components/atoms/token-icon";
import { Text } from "../../../../../components/atoms/typography/text";
-import type { TokenBalanceScanResponseDto } from "../../../../../domain/types/token-balance";
import { useTrackEvent } from "../../../../../hooks/tracking/use-track-event";
+import type { EarnTokenOption } from "../../state/effect-atom-poc/types";
import { selectItemText } from "../../styles.css";
type Props = {
- item: TokenBalanceScanResponseDto;
+ item: EarnTokenOption;
isConnected: boolean;
isSelected: boolean;
- onTokenBalanceSelect: (tokenBalance: TokenBalanceScanResponseDto) => void;
+ onTokenBalanceSelect: (tokenBalance: EarnTokenOption) => void;
};
export const SelectTokenListItem = memo(
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 68f939cd..83b8152e 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
@@ -48,9 +48,7 @@ export const SelectValidatorSection = () => {
selectedStake={val.selectedStake}
/>
}
- selectedValidators={
- new Set(selectedValidatorsArr.map((v) => v.address))
- }
+ selectedValidators={new Set(selectedValidatorsArr.map((v) => v.key))}
multiSelect={multiSelect}
selectedStake={val.selectedStake}
onItemClick={onItemClick}
diff --git a/packages/widget/src/pages/details/earn-page/components/select-validator-section/select-validator-trigger.tsx b/packages/widget/src/pages/details/earn-page/components/select-validator-section/select-validator-trigger.tsx
index 92f9da4c..4a653b26 100644
--- a/packages/widget/src/pages/details/earn-page/components/select-validator-section/select-validator-trigger.tsx
+++ b/packages/widget/src/pages/details/earn-page/components/select-validator-section/select-validator-trigger.tsx
@@ -8,8 +8,8 @@ import { PreferredIcon } from "../../../../../components/atoms/icons/preferred";
import { XIcon } from "../../../../../components/atoms/icons/x-icon";
import { Image } from "../../../../../components/atoms/image";
import { Text } from "../../../../../components/atoms/typography/text";
+import type { Validator } from "../../../../../domain/types/validators";
import type { Yield } from "../../../../../domain/types/yields";
-import type { ValidatorDto } from "../../../../../generated/api/yield";
import { APToPercentage } from "../../../../../utils";
import { formatCompactNumber } from "../../../../../utils/formatters";
import {
@@ -42,7 +42,7 @@ const getDisplayWebsite = (website: string) => {
const formatCommission = (commission: number | undefined) =>
typeof commission === "number" ? `${APToPercentage(commission)}%` : null;
-const formatValidatorTvl = (validator: ValidatorDto, selectedStake: Yield) => {
+const formatValidatorTvl = (validator: Validator, selectedStake: Yield) => {
const tvl = validator.tvl ?? validator.tvlRaw;
if (!tvl) return null;
@@ -60,9 +60,9 @@ export const SelectValidatorTrigger = ({
selectedValidatorsArr,
selectedStake,
}: {
- onRemoveValidator: (item: ValidatorDto) => void;
+ onRemoveValidator: (item: Validator) => void;
multiSelect: boolean;
- selectedValidatorsArr: ValidatorDto[];
+ selectedValidatorsArr: Validator[];
selectedStake: Yield;
}) => {
const { t } = useTranslation();
@@ -84,7 +84,7 @@ export const SelectValidatorTrigger = ({
return (
@@ -180,7 +180,7 @@ export const SelectValidatorTrigger = ({
return (
diff --git a/packages/widget/src/pages/details/earn-page/components/select-validator-section/use-select-validator.ts b/packages/widget/src/pages/details/earn-page/components/select-validator-section/use-select-validator.ts
index ca3fdfc0..8f49913b 100644
--- a/packages/widget/src/pages/details/earn-page/components/select-validator-section/use-select-validator.ts
+++ b/packages/widget/src/pages/details/earn-page/components/select-validator-section/use-select-validator.ts
@@ -1,4 +1,4 @@
-import type { ValidatorDto } from "../../../../../generated/api/yield";
+import type { Validator } from "../../../../../domain/types/validators";
import { useTrackEvent } from "../../../../../hooks/tracking/use-track-event";
import { useEarnPageContext } from "../../state/earn-page-context";
@@ -26,7 +26,7 @@ export const useSelectValidator = () => {
const onClose = () => trackEvent("selectValidatorModalClosed");
const onOpen = () => trackEvent("selectValidatorModalOpened");
- const onItemClick = (item: ValidatorDto) => {
+ const onItemClick = (item: Validator) => {
trackEvent("validatorSelected", {
validatorName: item.name,
validatorAddress: item.address,
@@ -34,7 +34,7 @@ export const useSelectValidator = () => {
onValidatorSelect(item);
};
- const onRemoveValidator = (item: ValidatorDto) => {
+ const onRemoveValidator = (item: Validator) => {
trackEvent("validatorRemoved", {
validatorName: item.name,
validatorAddress: item.address,
diff --git a/packages/widget/src/pages/details/earn-page/components/select-yield-section/index.tsx b/packages/widget/src/pages/details/earn-page/components/select-yield-section/index.tsx
index d9d80b99..3deaedd0 100644
--- a/packages/widget/src/pages/details/earn-page/components/select-yield-section/index.tsx
+++ b/packages/widget/src/pages/details/earn-page/components/select-yield-section/index.tsx
@@ -46,9 +46,7 @@ export const SelectYieldSection = () => {
) : (
selectedStakeData
.map((val) => {
- const opportunityCount = dashboardVariant
- ? val.filtered.length
- : val.all.length;
+ const opportunityCount = val.all.length;
return opportunityCount === 0 ? (
{
const name = getValidatorName(validator);
return {
- key: validator.address,
+ key: validator.key,
logo: providerDetails?.logo ?? validator.logoURI,
name: providerDetails?.name ?? name,
};
@@ -262,5 +262,5 @@ const YieldStrategyDetails = ({
);
};
-const getValidatorName = (validator: ValidatorDto) =>
+const getValidatorName = (validator: Validator) =>
validator.name ?? validator.address;
diff --git a/packages/widget/src/pages/details/earn-page/earn.page.tsx b/packages/widget/src/pages/details/earn-page/earn.page.tsx
index 9876b9e8..43578174 100644
--- a/packages/widget/src/pages/details/earn-page/earn.page.tsx
+++ b/packages/widget/src/pages/details/earn-page/earn.page.tsx
@@ -22,7 +22,6 @@ import {
EarnPageContextProvider,
useEarnPageContext,
} from "./state/earn-page-context";
-import { EarnPageStateUsageBoundaryProvider } from "./state/earn-page-state-context";
const EarnKycGateSection = () => {
const { kycGate, kycGateIsChecking, kycProviderName, onKycStatusRefresh } =
@@ -89,11 +88,9 @@ const EarnPageComponent = () => {
};
const EarnPage = () => (
-
-
-
-
-
+
+
+
);
export const AnimatedEarnPage = () => {
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 91d17c2a..c32e8785 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
@@ -1,11 +1,13 @@
+import { useAtom, useAtomValue } from "@effect/atom-react";
import { useConnectModal } from "@stakekit/rainbowkit";
import { useMutation } from "@tanstack/react-query";
import BigNumber from "bignumber.js";
+import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
+import type * as Atom from "effect/unstable/reactivity/Atom";
import { List, Maybe } from "purify-ts";
import type { PropsWithChildren } from "react";
import {
createContext,
- useCallback,
useContext,
useDeferredValue,
useEffect,
@@ -21,12 +23,13 @@ import {
tokenString,
} from "../../../../domain";
import { getKycProviderName } from "../../../../domain/types/kyc";
-import { getInitSelectedValidators } from "../../../../domain/types/stake";
-import type { TokenBalanceScanResponseDto } from "../../../../domain/types/token-balance";
+import type { PositionsData } from "../../../../domain/types/positions";
import type { TronResourceType } from "../../../../domain/types/tron";
+import type { Validator } from "../../../../domain/types/validators";
import {
type DashboardYieldCategory,
type ExtendedYieldType,
+ filterValidators,
getDashboardYieldCategory,
getExtendedYieldType,
getYieldRewardTokens,
@@ -35,17 +38,10 @@ import {
isBittensorStaking,
isNonZeroRewardRateYield,
isYieldActionArgRequired,
- isYieldValidatorSelectionRequired,
type YieldBase,
} from "../../../../domain/types/yields";
-import type { ValidatorDto } from "../../../../generated/api/yield";
-import { useDefaultTokens } from "../../../../hooks/api/use-default-tokens";
-import { useStreamYieldSummaries } from "../../../../hooks/api/use-multi-yields";
-import { useTokenBalancesScan } from "../../../../hooks/api/use-token-balances-scan";
import { useTokensPrices } from "../../../../hooks/api/use-tokens-prices";
import { useYieldKycGate } from "../../../../hooks/api/use-yield-kyc-gate";
-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 {
getPositionDetailsStakeReviewPath,
@@ -57,12 +53,12 @@ import { useDebouncedValue } from "../../../../hooks/use-debounced-value";
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";
import { useRewardTokenDetails } from "../../../../hooks/use-reward-token-details";
import { useSavedRef } from "../../../../hooks/use-saved-ref";
+import { useValidatorsConfig } from "../../../../hooks/use-validators-config";
import { useYieldType } from "../../../../hooks/use-yield-type";
-import { useEnterStakeStore } from "../../../../providers/enter-stake-store";
+import { useSetEnterStakeRequest } from "../../../../providers/enter-stake-store";
import { useMountAnimation } from "../../../../providers/mount-animation";
import { useSettings } from "../../../../providers/settings";
import { useSKWallet } from "../../../../providers/sk-wallet";
@@ -70,12 +66,11 @@ import { useWagmiConfig } from "../../../../providers/wagmi";
import { defaultFormattedNumber, formatNumber } from "../../../../utils";
import type { PageCta } from "../../../components/page-cta";
import type { SelectedStakeData } from "../types";
-import {
- useEarnPageDispatch,
- useEarnPageState,
-} from "./earn-page-state-context";
+import { YieldValidatorsPullKey } from "./effect-atom-poc/catalog/keys";
+import { useEarnMachine } from "./effect-atom-poc/hooks/use-earn-machine";
+import type { EarnTokenOption } from "./effect-atom-poc/types";
import type { EarnPageContextType } from "./types";
-import { useInitYield } from "./use-init-yield";
+import { useAmountValidation } from "./use-amount-validation";
import { usePendingActionDeepLink } from "./use-pending-action-deep-link";
import { useStakeEnterRequestDto } from "./use-stake-enter-request-dto";
@@ -83,53 +78,111 @@ const EarnPageContext = createContext(
undefined
);
+const getAsyncValue = (result: AsyncResult.AsyncResult) =>
+ AsyncResult.getOrElse(result, () => null as A | null);
+
+const getPullItems = (result: Atom.PullResult): A[] =>
+ getAsyncValue(result)?.items ?? [];
+
+const isAsyncErrorWithoutValue = (
+ result: AsyncResult.AsyncResult
+) => AsyncResult.isFailure(result) && getAsyncValue(result) === null;
+
+const maybeFromNullable = (value: A | null | undefined) =>
+ Maybe.fromNullable(value);
+
export const EarnPageContextProvider = ({
children,
registerFooterButton = true,
}: PropsWithChildren<{ registerFooterButton?: boolean }>) => {
- const {
- actions: { onMaxClick: _onMaxClick },
- selectedToken,
- selectedStakeId,
- autoSelectYield,
- selectedValidators,
- stakeAmount,
- selectedStake,
- tronResource,
- stakeAmountGreaterThanAvailableAmount,
- stakeAmountGreaterThanMax,
- stakeAmountLessThanMin,
- stakeAmountIsZero,
- availableAmount,
- availableYields,
- hasNotYieldsForToken,
- selectedProviderYieldId,
- selectedDashboardYieldCategory,
- availableDashboardYieldCategories,
- } = useEarnPageState();
- const dispatch = useEarnPageDispatch();
-
const { t } = useTranslation();
const initParams = useInitParams();
- const { dashboardVariant, externalProviders, variant, yieldGrouping } =
- useSettings();
+ const {
+ dashboardVariant,
+ dashboardYieldCategoryOrder,
+ externalProviders,
+ preferredTokenYieldsPerNetwork,
+ tokensForEnabledYieldsOnly,
+ variant,
+ yieldGrouping,
+ } = useSettings();
const dashboardYieldCategoryGroupingEnabled =
- dashboardVariant && yieldGrouping === "category";
+ !!dashboardVariant && yieldGrouping === "category";
+ const validatorsConfig = useValidatorsConfig();
- const { isConnected, isConnecting, isLedgerLiveAccountPlaceholder, chain } =
- useSKWallet();
+ const {
+ address,
+ additionalAddresses,
+ isConnected,
+ isConnecting,
+ isLedgerLiveAccountPlaceholder,
+ chain,
+ network,
+ } = useSKWallet();
+
+ const { view: machine, dispatch } = useEarnMachine({
+ address: isConnected ? address : null,
+ additionalAddresses,
+ categoryOrder: dashboardYieldCategoryOrder,
+ dashboardVariant: dashboardYieldCategoryGroupingEnabled,
+ initParams: initParams.data ?? null,
+ network: network ?? null,
+ preferredTokenYieldsPerNetwork: preferredTokenYieldsPerNetwork ?? null,
+ tokensForEnabledYieldsOnly: !!tokensForEnabledYieldsOnly,
+ });
+
+ const tokenOptionsResource = machine.resources.tokenOptions;
+ const tokenOptionsResult = useAtomValue(
+ tokenOptionsResource.loadedTokenOptionsAtom
+ );
+ const tokenOptions = getAsyncValue(tokenOptionsResult) ?? [];
+ const [tokenOptionsPull, pullMoreTokens] = useAtom(
+ tokenOptionsResource.tokenOptionsPullAtom
+ );
+ const positionsDataResult = useAtomValue(machine.resources.positionsDataAtom);
+ const positionsData =
+ getAsyncValue(positionsDataResult) ?? (new Map() as PositionsData);
+
+ const selectedTokenOption = maybeFromNullable(machine.selection.token);
+ const selectedToken = selectedTokenOption.map((option) => option.token);
+ const selectedStake = maybeFromNullable(machine.selection.yield);
+ const selectedStakeId = machine.selection.yield?.id ?? null;
+ const filteredSelectedValidators = selectedStake
+ .map((yieldDto) =>
+ filterValidators({
+ validatorsConfig,
+ validators: [...machine.selection.validators],
+ network: yieldDto.token.network,
+ yieldId: yieldDto.id,
+ })
+ )
+ .orDefault([...machine.selection.validators]);
+ const selectedValidators = new Map(
+ filteredSelectedValidators.map((validator) => [validator.key, validator])
+ );
+ const stakeAmount = new BigNumber(machine.form.stakeAmount);
+ const selectedProviderYieldId = maybeFromNullable(
+ machine.form.providerYieldId
+ );
+ const tronResource = maybeFromNullable(
+ machine.form.tronResource as TronResourceType | null
+ );
+ const selectedDashboardYieldCategory = machine.selection.category;
+ const availableDashboardYieldCategories =
+ dashboardYieldCategoryGroupingEnabled
+ ? [...machine.availableCategories]
+ : [];
+ const availableAmount = selectedTokenOption.map(
+ (tokenOption) => new BigNumber(tokenOption.amount)
+ );
+ const hasNotYieldsForToken = machine.status === "no-yields";
const yieldType = useYieldType(selectedStake).mapOrDefault(
(y) => y.title,
""
);
- const initYieldRes = useInitYield({
- selectedDashboardYieldCategory,
- selectedToken,
- });
-
const estimatedRewards = useEstimatedRewards({
selectedStake,
selectedValidators,
@@ -205,88 +258,45 @@ export const EarnPageContextProvider = ({
const validatorSearchDebouncing =
normalizedValidatorSearch !== debouncedValidatorSearch;
- const yieldSummaries = useStreamYieldSummaries(
- useMemo(() => availableYields.orDefault([]), [availableYields])
+ const validatorsResource = machine.resources.validators;
+ const validatorsPullAtom = validatorsResource.validatorsPullAtom(
+ new YieldValidatorsPullKey({
+ search: debouncedValidatorSearch || null,
+ })
);
+ const loadedValidatorsMap = useAtomValue(
+ validatorsResource.loadedValidatorsAtom
+ );
+ const [validatorsPullResult, pullMoreValidators] =
+ useAtom(validatorsPullAtom);
- const tokenBalancesScan = useTokenBalancesScan();
- const defaultTokens = useDefaultTokens({
- yieldCategory: dashboardYieldCategoryGroupingEnabled
- ? selectedDashboardYieldCategory
- : null,
- });
+ const yieldOptions = machine.resources.yieldsResult
+ ? (getAsyncValue(machine.resources.yieldsResult) ?? [])
+ : [];
const tokenBalancesData = useMemo(
() =>
- Maybe.fromRecord({
- defTb: Maybe.fromNullable(defaultTokens.data).alt(Maybe.of([])),
- tb: Maybe.fromNullable(tokenBalancesScan.data).alt(Maybe.of([])),
- })
- .map((val) => {
- const categoryTokenSet =
- dashboardYieldCategoryGroupingEnabled &&
- selectedDashboardYieldCategory
- ? new Set(val.defTb.map((item) => tokenString(item.token)))
- : null;
- const tokenBalancesScanData = categoryTokenSet
- ? val.tb.filter((item) =>
- categoryTokenSet.has(tokenString(item.token))
- )
- : val.tb;
-
- const { tbWithAmount, tbWithoutAmount, tbSet } =
- tokenBalancesScanData.reduce(
- (acc, b) => {
- acc.tbSet.add(tokenString(b.token));
-
- if (new BigNumber(b.amount).isGreaterThan(0)) {
- acc.tbWithAmount.push(b);
- } else {
- acc.tbWithoutAmount.push(b);
- }
-
- return acc;
- },
- {
- tbSet: new Set(),
- tbWithAmount: [] as TokenBalanceScanResponseDto[],
- tbWithoutAmount: [] as TokenBalanceScanResponseDto[],
- }
- );
-
- return [
- ...tbWithAmount,
- ...tbWithoutAmount,
- ...val.defTb.filter((t) => !tbSet.has(tokenString(t.token))),
- ];
- })
- .chain((tb) =>
- Maybe.of(deferredTokenSearch)
- .chain((val) =>
- val.length >= 1 ? Maybe.of(val.toLowerCase()) : Maybe.empty()
- )
- .map((lowerSearch) => ({
- all: tb,
- filtered: tb.filter(
- (t) =>
- t.token.name.toLowerCase().includes(lowerSearch) ||
- t.token.symbol.toLowerCase().includes(lowerSearch)
- ),
- }))
- .alt(Maybe.of({ all: tb, filtered: tb }))
- ),
- [
- dashboardYieldCategoryGroupingEnabled,
- defaultTokens.data,
- deferredTokenSearch,
- selectedDashboardYieldCategory,
- tokenBalancesScan.data,
- ]
+ Maybe.of([...tokenOptions]).chain((tokens) =>
+ Maybe.of(deferredTokenSearch)
+ .chain((val) =>
+ val.length >= 1 ? Maybe.of(val.toLowerCase()) : Maybe.empty()
+ )
+ .map((lowerSearch) => ({
+ all: tokens,
+ filtered: tokens.filter(
+ (t) =>
+ t.token.name.toLowerCase().includes(lowerSearch) ||
+ t.token.symbol.toLowerCase().includes(lowerSearch)
+ ),
+ }))
+ .alt(Maybe.of({ all: tokens, filtered: tokens }))
+ ),
+ [deferredTokenSearch, tokenOptions]
);
const selectedStakeData = useMemo>(
() =>
- Maybe.of(yieldSummaries)
+ Maybe.of([...yieldOptions])
.map((val) =>
selectedStake
.filter(
@@ -386,92 +396,45 @@ export const EarnPageContextProvider = ({
dashboardYieldCategoryGroupingEnabled,
deferredStakeSearch,
selectedStake,
- yieldSummaries,
+ yieldOptions,
selectedDashboardYieldCategory,
t,
]
);
- const shouldFetchValidators = selectedStake
- .map(isYieldValidatorSelectionRequired)
- .orDefault(false);
-
- const yieldValidators = useYieldValidators({
- enabled: shouldFetchValidators,
- yieldId: selectedStake.extract()?.id,
- network: selectedStake.extract()?.token.network,
- search: debouncedValidatorSearch,
- });
-
- useEffect(() => {
- const currentYieldId = selectedStake.map((val) => val.id).extractNullable();
-
- if (!currentYieldId) {
- return;
- }
-
- if (selectedValidators.size > 0) {
- return;
- }
-
- const shouldSelectDefaultValidator = selectedStake
- .map(isYieldValidatorSelectionRequired)
- .orDefault(false);
-
- if (!shouldSelectDefaultValidator) {
- return;
- }
-
- if (!yieldValidators.isFetched && !yieldValidators.isError) {
- 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 shouldFetchValidators = validatorsResource.enabled;
const validatorsData = useMemo(
() =>
selectedStake
.filter(() => shouldFetchValidators)
- .chain(() =>
- Maybe.fromNullable(yieldValidators.data).map((validators) => {
- if (
- dashboardVariant ||
- variant === "utila" ||
- variant === "porto"
- ) {
- return [...validators].sort(
- (a, b) =>
- (b.rewardRate?.total ?? 0) - (a.rewardRate?.total ?? 0)
- );
- }
+ .map((yieldDto) => {
+ const validators = filterValidators({
+ validatorsConfig,
+ validators: debouncedValidatorSearch
+ ? getPullItems(validatorsPullResult)
+ : [...loadedValidatorsMap.values()],
+ network: yieldDto.token.network,
+ yieldId: yieldDto.id,
+ });
+
+ if (dashboardVariant || variant === "utila" || variant === "porto") {
+ return [...validators].sort(
+ (a, b) => (b.rewardRate?.total ?? 0) - (a.rewardRate?.total ?? 0)
+ );
+ }
- return validators;
- })
- ),
+ return validators;
+ }),
[
dashboardVariant,
+ debouncedValidatorSearch,
+ loadedValidatorsMap,
selectedStake,
shouldFetchValidators,
variant,
- yieldValidators.data,
+ validatorsConfig,
+ validatorsPullResult,
]
);
@@ -484,16 +447,14 @@ export const EarnPageContextProvider = ({
const onValidatorSearch: SelectModalProps["onSearch"] = (val) =>
setValidatorSearch(val);
- const onTokenBalanceSelect = useCallback(
- (tokenBalance: TokenBalanceScanResponseDto) =>
- dispatch({ type: "tokenBalance/select", data: tokenBalance }),
- [dispatch]
- );
+ const onTokenBalanceSelect = (tokenBalance: EarnTokenOption) =>
+ dispatch({
+ type: "token/select",
+ tokenKey: tokenString(tokenBalance.token),
+ });
const onYieldSelect = (yieldId: string) => {
- Maybe.fromNullable(yieldSummaries)
- .chain((val) => List.find((v) => v.id === yieldId, val))
- .ifJust((val) => dispatch({ type: "yield/select", data: val }));
+ dispatch({ type: "yield/select", yieldId });
};
const onDashboardYieldCategorySelect = (category: DashboardYieldCategory) => {
@@ -502,25 +463,42 @@ export const EarnPageContextProvider = ({
if (selectedDashboardYieldCategory === category) return;
dispatch({
- type: "dashboard/yield-category/select",
- data: category,
+ type: "category/select",
+ category,
});
};
- const onValidatorSelect = (item: ValidatorDto) =>
+ const onValidatorSelect = (item: Validator) =>
selectedStake.ifJust((ss) =>
isYieldActionArgRequired(ss, "enter", "validatorAddresses")
- ? dispatch({ type: "validator/multiselect", data: item })
- : dispatch({ type: "validator/select", data: item })
+ ? dispatch({
+ type: "validator/multiselect",
+ validatorKey: item.key,
+ })
+ : dispatch({
+ type: "validator/select",
+ validatorKey: item.key,
+ })
);
- const onValidatorRemove = (item: ValidatorDto) =>
- dispatch({ type: "validator/remove", data: item });
+ const onValidatorRemove = (item: Validator) =>
+ dispatch({ type: "validator/remove", validatorKey: item.key });
const onStakeAmountChange: NumberInputProps["onChange"] = (val) =>
- dispatch({ type: "stakeAmount/change", data: val });
+ dispatch({ type: "stakeAmount/change", amount: val.toString(10) });
+
+ const onProviderYieldIdSelect = (yieldId: string) =>
+ dispatch({ type: "providerYieldId/select", providerYieldId: yieldId });
- const stakeEnterRequestDto = useStakeEnterRequestDto();
+ const stakeEnterRequestDto = useStakeEnterRequestDto({
+ selectedProviderYieldId,
+ selectedStake,
+ selectedToken,
+ selectedValidators,
+ stakeAmount,
+ tronResource,
+ useMaxAmount: machine.form.useMaxAmount,
+ });
const yieldKycGate = useYieldKycGate({ yieldDto: selectedStake });
const kycGateIsBlocking = yieldKycGate.isGateBlocking;
const kycProviderName = selectedStake
@@ -536,7 +514,7 @@ export const EarnPageContextProvider = ({
balanceId: positionDetailsStakeMatch?.params.balanceId,
integrationId: positionDetailsStakeMatch?.params.integrationId,
});
- const enterStakeStore = useEnterStakeStore();
+ const setEnterStakeRequest = useSetEnterStakeRequest();
const onClickHandler = useMutation({
mutationFn: async () => {
@@ -551,17 +529,17 @@ export const EarnPageContextProvider = ({
selectedToken,
}).unsafeCoerce();
- enterStakeStore.send({
- type: "initFlow",
- data: {
+ setEnterStakeRequest(
+ Maybe.of({
+ actionDto: Maybe.empty(),
addresses: val.stakeEnterRequestDto.addresses,
requestDto: val.stakeEnterRequestDto.dto,
selectedToken: val.selectedToken,
gasFeeToken: val.stakeEnterRequestDto.gasFeeToken,
selectedStake: val.stakeEnterRequestDto.selectedStake,
selectedValidators: val.stakeEnterRequestDto.selectedValidators,
- },
- });
+ })
+ );
navigate(positionDetailsStakeReviewPath ?? "/review");
},
});
@@ -571,7 +549,32 @@ export const EarnPageContextProvider = ({
// biome-ignore lint: false
useEffect(() => {
onClickHandlerResetRef.current();
- }, [isConnected, selectedStake, onClickHandlerResetRef]);
+ }, [isConnected, selectedStakeId, onClickHandlerResetRef]);
+
+ const {
+ maxIntegrationAmount,
+ minIntegrationAmount,
+ minEnterOrExitAmount,
+ maxEnterOrExitAmount,
+ isForceMax,
+ } = useMaxMinYieldAmount({
+ type: "enter",
+ yieldOpportunity: selectedStake,
+ availableAmount,
+ positionsData,
+ });
+
+ const {
+ stakeAmountGreaterThanAvailableAmount,
+ stakeAmountGreaterThanMax,
+ stakeAmountLessThanMin,
+ stakeAmountIsZero,
+ } = useAmountValidation({
+ availableAmount,
+ stakeAmount,
+ maxEnterOrExitAmount,
+ minEnterOrExitAmount,
+ });
const validation = useMemo(() => {
const val = {
@@ -620,19 +623,6 @@ export const EarnPageContextProvider = ({
tronResource,
]);
- const {
- maxIntegrationAmount,
- minIntegrationAmount,
- minEnterOrExitAmount,
- maxEnterOrExitAmount,
- isForceMax,
- } = useMaxMinYieldAmount({
- type: "enter",
- yieldOpportunity: selectedStake,
- availableAmount,
- positionsData: usePositionsData().data,
- });
-
const stakeMaxAmount = useMemo(
() =>
selectedStake
@@ -659,9 +649,9 @@ export const EarnPageContextProvider = ({
const { state } = useMountAnimation();
- const yieldOpportunityLoading = useYieldOpportunity(
- selectedStake.extract()?.id
- ).isLoading;
+ const yieldOpportunityLoading =
+ machine.status === "loading-yields" ||
+ !!machine.resources.yieldsResult?.waiting;
const appLoading =
selectedToken.isNothing() ||
@@ -671,12 +661,20 @@ export const EarnPageContextProvider = ({
isConnecting ||
!state.layout;
- const tokenBalancesScanLoading = tokenBalancesScan.isLoading;
- const defaultTokensIsLoading = defaultTokens.isLoading;
+ const tokenOptionsLoading =
+ tokenOptionsResult.waiting && tokenOptions.length === 0;
- const isFetching = tokenBalancesScan.isFetching;
+ const isFetching =
+ tokenOptionsResult.waiting ||
+ positionsDataResult.waiting ||
+ !!machine.resources.yieldsResult?.waiting;
- const isError = !tokenBalancesScan.data && tokenBalancesScan.isError;
+ const isError =
+ isAsyncErrorWithoutValue(tokenOptionsResult) ||
+ isAsyncErrorWithoutValue(positionsDataResult) ||
+ (machine.resources.yieldsResult
+ ? isAsyncErrorWithoutValue(machine.resources.yieldsResult)
+ : false);
const buttonDisabled =
isConnected &&
@@ -697,11 +695,14 @@ export const EarnPageContextProvider = ({
const onMaxClick = () => {
trackEvent("earnPageMaxClicked");
- _onMaxClick();
+ dispatch({
+ type: "stakeAmount/max",
+ amount: maxEnterOrExitAmount.toString(10),
+ });
};
const onTronResourceSelect = (value: TronResourceType) =>
- dispatch({ type: "tronResource/select", data: value });
+ dispatch({ type: "tronResource/select", tronResource: value });
const onClickRef = useSavedRef(onClickHandler.mutate);
@@ -730,29 +731,37 @@ export const EarnPageContextProvider = ({
[selectedStake, selectedToken]
);
+ const tokenPullValue = getAsyncValue(tokenOptionsPull);
+ const validatorPullValue = getAsyncValue(validatorsPullResult);
+ const hasMoreTokens = tokenPullValue?.done === false;
+ const hasMoreValidators = validatorPullValue?.done === false;
+ const isLoadingMoreTokens =
+ tokenOptionsPull.waiting && getPullItems(tokenOptionsPull).length > 0;
+ const isLoadingMoreValidators =
+ validatorsPullResult.waiting &&
+ getPullItems(validatorsPullResult).length > 0;
+ const onLoadMoreTokens = () => pullMoreTokens();
+ const onLoadMoreValidators = () => pullMoreValidators();
+
const selectTokenIsLoading =
- tokenBalancesScanLoading || defaultTokensIsLoading;
+ machine.status === "loading-token-options" ||
+ machine.status === "loading-initial-selection" ||
+ tokenOptionsLoading;
const selectYieldIsLoading =
- (autoSelectYield && selectedStakeId.isNothing() && !hasNotYieldsForToken) ||
- initYieldRes.isLoading ||
+ machine.status === "loading-initial-selection" ||
yieldOpportunityLoading ||
- tokenBalancesScanLoading ||
- defaultTokensIsLoading;
+ tokenOptionsLoading;
const selectValidatorIsLoading =
- defaultTokensIsLoading ||
- tokenBalancesScanLoading ||
- initYieldRes.isLoading ||
+ tokenOptionsLoading ||
yieldOpportunityLoading ||
validatorSearchDebouncing ||
- (shouldFetchValidators && yieldValidators.isLoading);
+ (shouldFetchValidators &&
+ validatorsPullResult.waiting &&
+ getPullItems(validatorsPullResult).length === 0);
- const footerIsLoading =
- defaultTokensIsLoading ||
- tokenBalancesScanLoading ||
- initYieldRes.isLoading ||
- yieldOpportunityLoading;
+ const footerIsLoading = tokenOptionsLoading || yieldOpportunityLoading;
const cta = useMemo(
() =>
@@ -795,18 +804,22 @@ export const EarnPageContextProvider = ({
);
const value = {
+ machine,
+ machineStatus: machine.status,
cta,
selectedTokenAvailableAmount,
formattedPrice,
symbol,
selectedStakeData,
selectedStake,
+ selectedProviderYieldId,
selectedDashboardYieldCategory,
availableDashboardYieldCategories,
onDashboardYieldCategorySelect,
onYieldSelect,
onTokenBalanceSelect,
onStakeAmountChange,
+ onProviderYieldIdSelect,
estimatedRewards,
yieldType,
onMaxClick,
@@ -833,7 +846,6 @@ export const EarnPageContextProvider = ({
isConnected,
appLoading,
yieldOpportunityLoading,
- tokenBalancesScanLoading,
tokenBalancesData,
onTokenSearch,
onValidatorSearch,
@@ -841,11 +853,10 @@ export const EarnPageContextProvider = ({
providersDetails,
tokenSearch,
stakeSearch,
- defaultTokensIsLoading,
- hasMoreTokens: !!defaultTokens.hasNextPage,
+ hasMoreTokens,
isLedgerLiveAccountPlaceholder,
- isLoadingMoreTokens: defaultTokens.isFetchingNextPage,
- onLoadMoreTokens: defaultTokens.fetchNextPage,
+ isLoadingMoreTokens,
+ onLoadMoreTokens,
tronResource,
onTronResourceSelect,
validation,
@@ -858,9 +869,9 @@ export const EarnPageContextProvider = ({
stakeMinAmount,
selectedToken,
validatorsData,
- hasMoreValidators: !!yieldValidators.hasNextPage,
- isLoadingMoreValidators: yieldValidators.isFetchingNextPage,
- onLoadMoreValidators: yieldValidators.fetchNextPage,
+ hasMoreValidators,
+ isLoadingMoreValidators,
+ onLoadMoreValidators,
validatorSearch,
hasNotYieldsForToken,
isStakeTokenSameAsGasToken,
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
deleted file mode 100644
index 30417ae9..00000000
--- a/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx
+++ /dev/null
@@ -1,581 +0,0 @@
-import BigNumber from "bignumber.js";
-import { Maybe } from "purify-ts";
-import type { Dispatch, PropsWithChildren } from "react";
-import {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useReducer,
-} from "react";
-import { equalTokens } from "../../../../domain";
-import type { Networks } from "../../../../domain/types/chains/networks";
-import { isNetworkWithEnterMinBasedOnPosition } from "../../../../domain/types/stake";
-import type { TokenBalanceScanResponseDto } from "../../../../domain/types/token-balance";
-import type { TokenDto } from "../../../../domain/types/tokens";
-import {
- type DashboardYieldCategory,
- getDashboardYieldCategory,
- type Yield,
-} from "../../../../domain/types/yields";
-import { useDashboardYieldCatalog } from "../../../../hooks/api/use-dashboard-yield-catalog";
-import { useYieldOpportunity } from "../../../../hooks/api/use-yield-opportunity";
-import { useMaxMinYieldAmount } from "../../../../hooks/use-max-min-yield-amount";
-import { usePositionsData } from "../../../../hooks/use-positions-data";
-import { useSavedRef } from "../../../../hooks/use-saved-ref";
-import { useSettings } from "../../../../providers/settings";
-import { useSKWallet } from "../../../../providers/sk-wallet";
-import type { Actions, ExtraData, State } from "./types";
-import { useAmountValidation } from "./use-amount-validation";
-import { useGetInitYield } from "./use-get-init-yield";
-import { useInitToken } from "./use-init-token";
-import { useInitYield } from "./use-init-yield";
-import { useTokenBalance } from "./use-token-balance";
-import { useTrackStateEvents } from "./use-track-state-events";
-import { onYieldSelectState } from "./utils";
-
-const EarnPageStateContext = createContext<(State & ExtraData) | undefined>(
- undefined
-);
-const EarnPageDispatchContext = createContext | undefined>(
- undefined
-);
-const EarnPageStateUsageBoundary = createContext(false);
-
-export const EarnPageStateUsageBoundaryProvider = ({
- children,
-}: PropsWithChildren) => {
- return (
-
- {children}
-
- );
-};
-
-const getInitialState = (): State => ({
- selectedToken: Maybe.empty(),
- selectedStakeId: Maybe.empty(),
- autoSelectYield: true,
- selectedValidators: new Map(),
- stakeAmount: new BigNumber(0),
- useMaxAmount: false,
- tronResource: Maybe.empty(),
- selectedProviderYieldId: Maybe.empty(),
- selectedDashboardYieldCategory: null,
-});
-
-export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
- const { network, isConnected } = useSKWallet();
- const { dashboardVariant, yieldGrouping } = useSettings();
- const dashboardYieldCategoryGroupingEnabled =
- dashboardVariant && yieldGrouping === "category";
- const dashboardYieldCategorySelectionEnabled =
- !dashboardVariant || dashboardYieldCategoryGroupingEnabled;
-
- const getInitYield = useGetInitYield();
-
- const positionsData = usePositionsData();
- const dashboardYieldCatalog = useDashboardYieldCatalog({
- enabled: dashboardYieldCategoryGroupingEnabled,
- });
-
- const reducer = (state: State, action: Actions): State => {
- const onTokenSelectState = ({
- availableYields,
- token,
- }: {
- availableYields?: TokenBalanceScanResponseDto["availableYields"];
- token: TokenDto;
- }) =>
- Maybe.fromFalsy(
- state.selectedToken.map((v) => !equalTokens(v, token)).orDefault(true)
- )
- .chain(() =>
- getInitYield({
- availableYields,
- selectedDashboardYieldCategory:
- dashboardYieldCategorySelectionEnabled
- ? state.selectedDashboardYieldCategory
- : null,
- selectedToken: token,
- })
- .map<{
- selectedDashboardYieldCategory: DashboardYieldCategory | null;
- yieldState: ReturnType | null;
- }>((yieldDto) => ({
- selectedDashboardYieldCategory:
- dashboardYieldCategorySelectionEnabled
- ? state.selectedDashboardYieldCategory
- : null,
- yieldState: onYieldSelectState({
- yieldDto,
- positionsData: positionsData.data,
- }),
- }))
- .alt(
- Maybe.of({
- selectedDashboardYieldCategory:
- dashboardYieldCategorySelectionEnabled
- ? state.selectedDashboardYieldCategory
- : null,
- yieldState: null,
- })
- )
- )
- .map(({ selectedDashboardYieldCategory, yieldState }) => ({
- ...getInitialState(),
- selectedToken: Maybe.of(token),
- selectedDashboardYieldCategory,
- ...yieldState,
- }))
- .orDefault(state);
-
- switch (action.type) {
- case "token/select": {
- return onTokenSelectState({ token: action.data });
- }
-
- case "tokenBalance/select": {
- return onTokenSelectState({
- availableYields: action.data.availableYields,
- token: action.data.token,
- });
- }
-
- case "dashboard/yield-category/select": {
- if (!dashboardYieldCategoryGroupingEnabled) return state;
-
- const target = dashboardYieldCatalog.initialSelectionByCategory.get(
- action.data
- );
-
- if (!target) {
- return {
- ...state,
- selectedDashboardYieldCategory: action.data,
- };
- }
-
- return Maybe.fromFalsy(
- state.selectedDashboardYieldCategory !== action.data ||
- state.selectedToken
- .map((v) => !equalTokens(v, target.token))
- .orDefault(true) ||
- state.selectedStakeId
- .map((v) => v !== target.yieldDto.id)
- .orDefault(true)
- )
- .map(() =>
- onYieldSelectState({
- yieldDto: target.yieldDto,
- positionsData: positionsData.data,
- })
- )
- .map((val) => ({
- ...getInitialState(),
- selectedToken: Maybe.of(target.token),
- selectedDashboardYieldCategory: action.data,
- ...val,
- }))
- .orDefault(state);
- }
-
- case "positionDetails/stake/initialize": {
- return Maybe.fromFalsy(
- state.selectedToken
- .map((v) => !equalTokens(v, action.data.token))
- .orDefault(true) ||
- state.selectedStakeId
- .map((v) => v !== action.data.id)
- .orDefault(true)
- )
- .map(() =>
- onYieldSelectState({
- yieldDto: action.data,
- positionsData: positionsData.data,
- })
- )
- .map((val) => ({
- ...getInitialState(),
- selectedToken: Maybe.of(action.data.token),
- selectedDashboardYieldCategory:
- dashboardYieldCategorySelectionEnabled
- ? getDashboardYieldCategory(action.data)
- : null,
- ...val,
- }))
- .orDefault(state);
- }
-
- case "yield/select": {
- return Maybe.fromFalsy(
- state.selectedStakeId.map((v) => v !== action.data.id).orDefault(true)
- )
- .map(() =>
- onYieldSelectState({
- yieldDto: action.data,
- positionsData: positionsData.data,
- })
- )
- .map((val) => ({
- ...getInitialState(),
- selectedToken: state.selectedToken,
- selectedDashboardYieldCategory:
- dashboardYieldCategorySelectionEnabled
- ? getDashboardYieldCategory(action.data)
- : null,
- ...val,
- }))
- .orDefault(state);
- }
-
- case "validator/select": {
- const selectedValidators = new Map();
- selectedValidators.set(action.data.address, action.data);
-
- return {
- ...state,
- selectedValidators,
- };
- }
-
- case "validator/multiselect": {
- const newMap = new Map(state.selectedValidators);
-
- if (newMap.has(action.data.address)) {
- newMap.delete(action.data.address);
- } else {
- newMap.set(action.data.address, action.data);
- }
-
- if (newMap.size === 0) return state;
-
- return {
- ...state,
- selectedValidators: newMap,
- };
- }
-
- case "providerYieldId/select": {
- return { ...state, selectedProviderYieldId: Maybe.of(action.data) };
- }
-
- case "validator/remove": {
- const selectedValidators = new Map(state.selectedValidators);
- selectedValidators.delete(action.data.address);
-
- return {
- ...state,
- selectedValidators,
- };
- }
-
- case "stakeAmount/change": {
- return {
- ...state,
- stakeAmount: action.data,
- useMaxAmount: false,
- };
- }
-
- case "stakeAmount/max": {
- return {
- ...state,
- stakeAmount: action.data,
- useMaxAmount: true,
- };
- }
-
- case "tronResource/select": {
- return { ...state, tronResource: Maybe.of(action.data) };
- }
-
- case "state/reset": {
- return getInitialState();
- }
-
- default:
- return state;
- }
- };
-
- const [state, dispatch] = useReducer(reducer, getInitialState());
-
- const {
- selectedToken,
- selectedStakeId,
- autoSelectYield,
- selectedValidators,
- stakeAmount: _stakeAmount,
- useMaxAmount,
- tronResource,
- selectedProviderYieldId,
- selectedDashboardYieldCategory: selectedDashboardYieldCategoryFallback,
- } = state;
-
- const initTokenRes = useInitToken();
- const initToken = useMemo(
- () => Maybe.fromNullable(initTokenRes.data),
- [initTokenRes.data]
- );
-
- const initYieldRes = useInitYield({
- selectedDashboardYieldCategory: dashboardYieldCategoryGroupingEnabled
- ? selectedDashboardYieldCategoryFallback
- : null,
- selectedToken,
- });
- const initYield = useMemo(
- () => Maybe.fromNullable(initYieldRes.data),
- [initYieldRes.data]
- );
-
- const yieldOpportunity = useYieldOpportunity(selectedStakeId.extract());
-
- const selectedStake = useMemo(
- () => Maybe.fromNullable(yieldOpportunity.data),
- [yieldOpportunity.data]
- );
- const selectedStakeDashboardYieldCategory = selectedStake
- .chainNullable(getDashboardYieldCategory)
- .extractNullable();
- const selectedDashboardYieldCategory = dashboardYieldCategorySelectionEnabled
- ? (selectedStakeDashboardYieldCategory ??
- selectedDashboardYieldCategoryFallback)
- : null;
-
- const { availableAmount, availableYields } = useTokenBalance({
- selectedDashboardYieldCategory: dashboardYieldCategoryGroupingEnabled
- ? selectedDashboardYieldCategory
- : null,
- selectedToken,
- });
-
- const { minEnterOrExitAmount, maxEnterOrExitAmount, isForceMax } =
- useMaxMinYieldAmount({
- type: "enter",
- yieldOpportunity: Maybe.fromNullable(yieldOpportunity.data),
- availableAmount,
- positionsData: positionsData.data,
- });
-
- /**
- * If stake amount is less then min, use min
- */
- const stakeAmount = useMemo(
- () => (isForceMax ? maxEnterOrExitAmount : _stakeAmount),
- [isForceMax, maxEnterOrExitAmount, _stakeAmount]
- );
-
- const setToken = useCallback(
- (token: TokenDto) => dispatch({ type: "token/select", data: token }),
- []
- );
-
- const setYield = useCallback(
- (yieldDto: Yield) => dispatch({ type: "yield/select", data: yieldDto }),
- []
- );
-
- /**
- * Reset selectedToken if we changed network
- */
- selectedToken.ifJust((ss) => {
- if (network && ss.network !== network) {
- dispatch({ type: "state/reset" });
- }
- });
-
- const selectedTokenRef = useSavedRef(selectedToken);
- const initTokenRef = useSavedRef(initToken);
- const initYieldRef = useSavedRef(initYield);
-
- const hasNotYieldsForToken =
- !initYieldRes.isLoading &&
- !initYieldRes.data &&
- initToken.isJust() &&
- selectedToken.isJust() &&
- initYield.isNothing() &&
- selectedStakeId.isNothing();
-
- useEffect(() => {
- if (!isConnected && selectedTokenRef.current.isJust()) {
- dispatch({ type: "state/reset" });
- }
- }, [selectedTokenRef, isConnected]);
-
- const shouldWaitForPositionsData = useMemo(
- () =>
- initToken
- .filter(
- (it) =>
- isNetworkWithEnterMinBasedOnPosition(it.network as Networks) &&
- positionsData.isPending
- )
- .isJust(),
- [initToken, positionsData]
- );
-
- /**
- * Set initial token
- */
- useEffect(() => {
- if (shouldWaitForPositionsData) return;
-
- initToken.ifJust(setToken);
- }, [shouldWaitForPositionsData, initToken, setToken]);
-
- useEffect(() => {
- if (shouldWaitForPositionsData) return;
-
- selectedToken.ifNothing(() => initTokenRef.current.ifJust(setToken));
- }, [shouldWaitForPositionsData, initTokenRef, selectedToken, setToken]);
-
- /**
- * Set initial yield
- */
- useEffect(() => {
- if (shouldWaitForPositionsData) return;
-
- if (!autoSelectYield) return;
-
- selectedStakeId.ifNothing(() => initYield.ifJust(setYield));
- }, [
- autoSelectYield,
- initYield,
- selectedStakeId,
- shouldWaitForPositionsData,
- setYield,
- ]);
-
- useEffect(() => {
- if (shouldWaitForPositionsData) return;
- if (!autoSelectYield) return;
-
- selectedStakeId.ifNothing(() => initYieldRef.current.ifJust(setYield));
- }, [
- autoSelectYield,
- initYieldRef,
- shouldWaitForPositionsData,
- selectedStakeId,
- setYield,
- ]);
-
- /**
- * Reset selectedToken if we dont have initToken
- * Case when we changed account, but we dont have available yields
- */
- useEffect(() => {
- initToken.ifNothing(() =>
- selectedToken.ifJust(() => dispatch({ type: "state/reset" }))
- );
- }, [initToken, selectedToken]);
-
- useTrackStateEvents({ initToken, initYield });
-
- const actions = useMemo(
- () => ({
- onMaxClick: () =>
- dispatch({ type: "stakeAmount/max", data: maxEnterOrExitAmount }),
- }),
- [maxEnterOrExitAmount]
- );
-
- const {
- stakeAmountGreaterThanAvailableAmount,
- stakeAmountGreaterThanMax,
- stakeAmountLessThanMin,
- stakeAmountIsZero,
- } = useAmountValidation({
- availableAmount,
- stakeAmount,
- maxEnterOrExitAmount,
- minEnterOrExitAmount,
- });
-
- const value: State & ExtraData = useMemo(
- () => ({
- selectedStakeId,
- autoSelectYield,
- selectedStake,
- selectedValidators,
- stakeAmount,
- useMaxAmount,
- actions,
- tronResource,
- stakeAmountGreaterThanAvailableAmount,
- stakeAmountGreaterThanMax,
- stakeAmountLessThanMin,
- stakeAmountIsZero,
- availableAmount,
- availableYields,
- selectedToken,
- hasNotYieldsForToken,
- selectedProviderYieldId,
- selectedDashboardYieldCategory,
- availableDashboardYieldCategories: dashboardYieldCategoryGroupingEnabled
- ? dashboardYieldCatalog.availableCategories
- : [],
- }),
- [
- selectedStakeId,
- autoSelectYield,
- selectedStake,
- selectedToken,
- selectedValidators,
- stakeAmount,
- useMaxAmount,
- actions,
- tronResource,
- stakeAmountGreaterThanAvailableAmount,
- stakeAmountGreaterThanMax,
- stakeAmountLessThanMin,
- stakeAmountIsZero,
- availableAmount,
- availableYields,
- hasNotYieldsForToken,
- selectedProviderYieldId,
- selectedDashboardYieldCategory,
- dashboardYieldCategoryGroupingEnabled,
- dashboardYieldCatalog.availableCategories,
- ]
- );
-
- return (
-
-
- {children}
-
-
- );
-};
-
-const useUsageCheck = () => {
- const check = useContext(EarnPageStateUsageBoundary);
- if (!check) {
- throw new Error("hook must be used withing EarnPageStateUsageBoundary");
- }
-};
-
-export const useEarnPageState = () => {
- useUsageCheck();
-
- const state = useContext(EarnPageStateContext);
- if (state === undefined) {
- throw new Error("useState must be used within a StateProvider");
- }
-
- return state;
-};
-
-export const useEarnPageDispatch = () => {
- useUsageCheck();
-
- const dispatch = useContext(EarnPageDispatchContext);
- if (dispatch === undefined) {
- throw new Error("useDispatch must be used within a StateProvider");
- }
-
- return dispatch;
-};
diff --git a/packages/widget/src/pages/details/earn-page/state/effect-atom-poc/catalog/atoms.ts b/packages/widget/src/pages/details/earn-page/state/effect-atom-poc/catalog/atoms.ts
new file mode 100644
index 00000000..ac1a7d9a
--- /dev/null
+++ b/packages/widget/src/pages/details/earn-page/state/effect-atom-poc/catalog/atoms.ts
@@ -0,0 +1,818 @@
+import BigNumber from "bignumber.js";
+import { Cause, Duration, Effect, flow, Option, Stream } from "effect";
+import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
+import * as Atom from "effect/unstable/reactivity/Atom";
+import { tokenString } from "../../../../../../domain";
+import type { Networks } from "../../../../../../domain/types/chains/networks";
+import {
+ type BalanceDataKey,
+ getPositionBalanceDataKey,
+ type PositionsData,
+ type PositionValidators,
+ toYieldBalancesByYields,
+ type YieldBalanceDto,
+ type YieldBalancesByYieldDto,
+} from "../../../../../../domain/types/positions";
+import type { TokenBalanceScanDto } from "../../../../../../domain/types/token-balance";
+import type { TokenDto } from "../../../../../../domain/types/tokens";
+import { toValidators } from "../../../../../../domain/types/validators";
+import {
+ type DashboardYieldCategory,
+ getApiYieldTypesForDashboardCategory,
+ isNonZeroRewardRateYield,
+} from "../../../../../../domain/types/yields";
+import type {
+ BalancesRequestDto,
+ YieldDto,
+} from "../../../../../../generated/api/yield";
+import { StakeKitApiService, widgetAtomRuntime } from "../runtime";
+import {
+ EarnCatalogError,
+ type EarnCatalogOperation,
+ type EarnCatalogUnderlyingError,
+ type EarnTokenOption,
+ type EarnTokenOptionsState,
+ type EarnValidatorKey,
+ type EarnValidatorOption,
+ type EarnValidatorsPullParams,
+ type EarnValidatorsResource,
+} from "../types";
+import {
+ type AvailableYieldCategoriesKey,
+ DefaultTokenOptionsKey,
+ InitTokenOptionKey,
+ InitYieldKey,
+ type PositionsDataKey,
+ TokenBalancesScanKey,
+ type TokenOptionsKey,
+ TokenYieldScopeKey,
+ type YieldCatalogKey,
+ type YieldValidatorsKey,
+} from "./keys";
+import { loadAllPages, loadAllPagesByIdChunks } from "./utilities";
+
+const catalogSWR = flow(
+ Atom.swr({
+ staleTime: Duration.minutes(5),
+ revalidateOnMount: true,
+ }),
+ Atom.setIdleTTL(Duration.minutes(5))
+);
+
+const DEFAULT_PAGE_SIZE = 100;
+const YIELD_IDS_CHUNK_SIZE = 100;
+const PREFERRED_PAGE_CONCURRENCY = 5;
+
+const toCatalogError =
+ (operation: EarnCatalogOperation) => (cause: EarnCatalogUnderlyingError) =>
+ new EarnCatalogError({ operation, cause });
+
+const withCatalogError =
+ (operation: EarnCatalogOperation) =>
+ (
+ effect: Effect.Effect
+ ) =>
+ effect.pipe(Effect.mapError(toCatalogError(operation)));
+
+const mapCatalogStreamError =
+ (operation: EarnCatalogOperation) =>
+ (
+ stream: Stream.Stream
+ ) =>
+ stream.pipe(Stream.mapError(toCatalogError(operation)));
+
+const toNetworksParam = (network: Networks | null) =>
+ network ? ([network] as const) : undefined;
+
+const toYieldTypesParam = (category: DashboardYieldCategory | null) =>
+ category ? getApiYieldTypesForDashboardCategory(category) : undefined;
+
+const shouldUseYieldTokensApi = ({
+ category,
+ tokensForEnabledYieldsOnly,
+}: {
+ category: DashboardYieldCategory | null;
+ tokensForEnabledYieldsOnly: boolean;
+}) => tokensForEnabledYieldsOnly || !!toYieldTypesParam(category)?.length;
+
+const getNextOffset = ({
+ itemsLength,
+ limit,
+ offset,
+ total,
+}: {
+ itemsLength: number;
+ limit: number;
+ offset: number;
+ total: number;
+}) => {
+ const nextOffset = offset + itemsLength;
+ return nextOffset < total && itemsLength >= limit
+ ? Option.some(nextOffset)
+ : Option.none();
+};
+
+const getBalanceValidators = (balance: YieldBalanceDto) =>
+ balance.validators ?? (balance.validator ? [balance.validator] : []);
+
+const toPositionsData = (balancesData: YieldBalancesByYieldDto[]) =>
+ balancesData.reduce((acc, val) => {
+ acc.set(val.yieldId, {
+ yieldId: val.yieldId,
+ rewardRate: val.rewardRate,
+ balanceData: [...val.balances]
+ .sort((a, b) =>
+ getPositionBalanceDataKey(a).localeCompare(
+ getPositionBalanceDataKey(b)
+ )
+ )
+ .reduce(
+ (acc, balance) => {
+ const key = getPositionBalanceDataKey(balance);
+ const prev = acc.get(key);
+ const validators = getBalanceValidators(balance);
+
+ if (prev) {
+ prev.balances.push(balance);
+ } else if (key === "default") {
+ acc.set(key, {
+ balances: [balance],
+ type: "default",
+ });
+ } else {
+ acc.set(key, {
+ balances: [balance],
+ type: "validators",
+ validators,
+ });
+ }
+
+ return acc;
+ },
+ new Map<
+ BalanceDataKey,
+ { balances: YieldBalanceDto[] } & (
+ | {
+ type: "validators";
+ validators: PositionValidators;
+ }
+ | { type: "default" }
+ )
+ >()
+ ),
+ });
+
+ return acc;
+ }, new Map() as PositionsData);
+
+export const availableYieldCategoriesAtom = Atom.family(
+ (key: AvailableYieldCategoriesKey) =>
+ widgetAtomRuntime
+ .atom(() =>
+ Effect.gen(function* () {
+ const api = yield* StakeKitApiService;
+
+ const availability = yield* Effect.all(
+ key.categoryOrder.map((category) =>
+ Effect.gen(function* () {
+ const response = yield* api.yield.YieldsControllerGetYields({
+ params: {
+ ...(key.network && { network: key.network }),
+ limit: DEFAULT_PAGE_SIZE,
+ types: getApiYieldTypesForDashboardCategory(category),
+ },
+ });
+
+ const hasVisibleYield = (response.items ?? []).some(
+ (yieldDto) =>
+ yieldDto.status.enter && isNonZeroRewardRateYield(yieldDto)
+ );
+
+ return hasVisibleYield ? category : null;
+ })
+ ),
+ { concurrency: PREFERRED_PAGE_CONCURRENCY }
+ );
+
+ return availability.filter(
+ (category): category is DashboardYieldCategory => category !== null
+ );
+ }).pipe(withCatalogError("available-yield-categories"))
+ )
+ .pipe(catalogSWR)
+);
+
+export const earnYieldCatalogAtom = Atom.family((key: YieldCatalogKey) => {
+ return widgetAtomRuntime
+ .atom(() =>
+ Effect.gen(function* () {
+ const api = yield* StakeKitApiService;
+
+ return yield* loadAllPagesByIdChunks({
+ chunkSize: YIELD_IDS_CHUNK_SIZE,
+ concurrency: PREFERRED_PAGE_CONCURRENCY,
+ fetchPage: ({ ids, offset }) =>
+ api.yield.YieldsControllerGetYields({
+ params: {
+ limit: DEFAULT_PAGE_SIZE,
+ offset,
+ types: toYieldTypesParam(key.category),
+ network: key.selectedToken.token.network,
+ yieldIds: ids,
+ },
+ }),
+ getItemId: (yieldDto) => yieldDto.id,
+ ids: key.selectedToken.availableYields,
+ pageSize: DEFAULT_PAGE_SIZE,
+ });
+ }).pipe(withCatalogError("earn-yield-catalog"))
+ )
+ .pipe(catalogSWR);
+});
+
+export const initYieldAtom = Atom.family((key: InitYieldKey) =>
+ widgetAtomRuntime
+ .atom(() =>
+ Effect.gen(function* () {
+ if (!key.yieldId) {
+ return null;
+ }
+
+ const api = yield* StakeKitApiService;
+
+ return yield* api.yield.YieldsControllerGetYield(
+ key.yieldId,
+ undefined
+ );
+ }).pipe(withCatalogError("init-yield"))
+ )
+ .pipe(catalogSWR)
+);
+
+export const positionsDataAtom = Atom.family((key: PositionsDataKey) =>
+ widgetAtomRuntime
+ .atom(() =>
+ Effect.gen(function* () {
+ if (!key.address || !key.network) {
+ return new Map() as PositionsData;
+ }
+
+ const api = yield* StakeKitApiService;
+ const response = yield* api.yield.YieldsControllerGetAggregateBalances({
+ payload: {
+ queries: [
+ {
+ address: key.address,
+ network:
+ key.network as BalancesRequestDto["queries"][number]["network"],
+ },
+ ],
+ },
+ });
+
+ return toPositionsData(toYieldBalancesByYields(response.items));
+ }).pipe(withCatalogError("positions-data"))
+ )
+ .pipe(catalogSWR)
+);
+
+const toDefaultTokenOption = (tokenWithYields: {
+ readonly token: TokenDto;
+ readonly availableYields: ReadonlyArray;
+}): EarnTokenOption => ({
+ token: tokenWithYields.token,
+ availableYields: tokenWithYields.availableYields,
+ amount: "0",
+ source: "default",
+});
+
+const toBalanceTokenOption = (tokenBalance: {
+ readonly token: TokenDto;
+ readonly availableYields: ReadonlyArray;
+ readonly amount: string;
+}): EarnTokenOption => ({
+ token: tokenBalance.token,
+ availableYields: tokenBalance.availableYields,
+ amount: tokenBalance.amount,
+ source: "balance",
+});
+
+const toInitTokenOption = (tokenWithYields: {
+ readonly token: TokenDto;
+ readonly availableYields: ReadonlyArray;
+}): EarnTokenOption => ({
+ token: tokenWithYields.token,
+ availableYields: tokenWithYields.availableYields,
+ amount: "0",
+ source: "init",
+});
+
+const toInitYieldTokenOption = (yieldDto: YieldDto): EarnTokenOption => ({
+ token: yieldDto.token,
+ availableYields: [yieldDto.id],
+ amount: "0",
+ source: "init",
+});
+
+const hasAvailableYields = (option: EarnTokenOption) =>
+ option.availableYields.length > 0;
+
+const getAvailableYieldIds = (
+ items: ReadonlyArray
+): ReadonlyArray =>
+ [...new Set(items.flatMap((option) => option.availableYields))].sort();
+
+const scopeTokenOptions = ({
+ items,
+ yieldIds,
+}: {
+ items: ReadonlyArray;
+ yieldIds: ReadonlySet | null;
+}) =>
+ items
+ .map((option) =>
+ yieldIds
+ ? {
+ ...option,
+ availableYields: option.availableYields.filter((yieldId) =>
+ yieldIds.has(yieldId)
+ ),
+ }
+ : option
+ )
+ .filter(hasAvailableYields);
+
+const getPullItemsResult = (
+ result: Atom.PullResult
+): AsyncResult.AsyncResult<
+ ReadonlyArray,
+ EarnCatalogError
+> => {
+ const itemsResult = result.pipe(
+ AsyncResult.map((value) => value.items as ReadonlyArray)
+ );
+ const error = itemsResult.pipe(AsyncResult.error, Option.getOrNull);
+
+ return Cause.isNoSuchElementError(error)
+ ? AsyncResult.success([], { waiting: itemsResult.waiting })
+ : (itemsResult as AsyncResult.AsyncResult<
+ ReadonlyArray,
+ EarnCatalogError
+ >);
+};
+
+const findInitTokenOption = ({
+ network,
+ token,
+ tokenOptions,
+}: {
+ network: Networks | null;
+ token: string;
+ tokenOptions: ReadonlyArray;
+}) =>
+ tokenOptions.find((option) => {
+ const tokenSymbolCompare =
+ token.toLowerCase() === option.token.symbol.toLowerCase();
+ const tokenNetworkCompare =
+ !!network && network.toLowerCase() === option.token.network.toLowerCase();
+ const tokenStringCompare = tokenString(option.token) === token;
+
+ return (tokenSymbolCompare && tokenNetworkCompare) || tokenStringCompare;
+ }) ?? null;
+
+const defaultTokenOptionsPullAtom = Atom.family((key: DefaultTokenOptionsKey) =>
+ widgetAtomRuntime.pull(() =>
+ Stream.paginate(0, (offset) =>
+ Effect.gen(function* () {
+ const api = yield* StakeKitApiService;
+
+ if (
+ shouldUseYieldTokensApi({
+ category: key.category,
+ tokensForEnabledYieldsOnly: key.tokensForEnabledYieldsOnly,
+ })
+ ) {
+ const response = yield* api.yield.TokensControllerGetTokens({
+ params: {
+ limit: DEFAULT_PAGE_SIZE,
+ offset,
+ networks: toNetworksParam(key.network),
+ yieldTypes: toYieldTypesParam(key.category),
+ },
+ });
+ const items = (response.items ?? []).map(toDefaultTokenOption);
+
+ return [
+ items,
+ getNextOffset({
+ itemsLength: response.items?.length ?? 0,
+ limit: response.limit,
+ offset: response.offset,
+ total: response.total,
+ }),
+ ] as const;
+ }
+
+ if (offset > 0) {
+ return [[], Option.none()] as const;
+ }
+
+ const response = yield* api.legacy.TokenControllerGetTokens({
+ params: {
+ network: key.network ?? undefined,
+ },
+ });
+
+ return [
+ response.map(toDefaultTokenOption),
+ Option.none(),
+ ] as const;
+ })
+ ).pipe(mapCatalogStreamError("default-token-options"))
+ )
+);
+
+const initTokenOptionAtom = Atom.family((key: InitTokenOptionKey) =>
+ widgetAtomRuntime
+ .atom(() =>
+ Effect.gen(function* () {
+ if (!key.token) {
+ return null;
+ }
+
+ const api = yield* StakeKitApiService;
+ const response = yield* api.legacy.TokenControllerGetTokens({
+ params: {
+ network: key.network ?? undefined,
+ },
+ });
+
+ return findInitTokenOption({
+ network: key.network,
+ token: key.token,
+ tokenOptions: response.map(toInitTokenOption),
+ });
+ }).pipe(withCatalogError("init-token-option"))
+ )
+ .pipe(catalogSWR)
+);
+
+const tokenBalancesScanAtom = Atom.family((key: TokenBalancesScanKey) =>
+ widgetAtomRuntime
+ .atom(() =>
+ Effect.gen(function* () {
+ if (!key.address || !key.network) {
+ return [];
+ }
+
+ const api = yield* StakeKitApiService;
+ const response = yield* api.legacy.TokenControllerTokenBalancesScan({
+ payload: {
+ addresses: {
+ address: key.address,
+ additionalAddresses: key.additionalAddresses ?? undefined,
+ },
+ network: key.network as TokenBalanceScanDto["network"],
+ },
+ });
+
+ return response.map(toBalanceTokenOption);
+ }).pipe(withCatalogError("token-balances-scan"))
+ )
+ .pipe(catalogSWR)
+);
+
+const tokenYieldScopeAtom = Atom.family((key: TokenYieldScopeKey) =>
+ widgetAtomRuntime
+ .atom(() =>
+ Effect.gen(function* () {
+ if (!key.category || key.yieldIds.length === 0) {
+ return new Set();
+ }
+
+ const api = yield* StakeKitApiService;
+ const yields = yield* loadAllPagesByIdChunks({
+ chunkSize: YIELD_IDS_CHUNK_SIZE,
+ concurrency: PREFERRED_PAGE_CONCURRENCY,
+ fetchPage: ({ ids, offset }) =>
+ api.yield.YieldsControllerGetYields({
+ params: {
+ limit: DEFAULT_PAGE_SIZE,
+ offset,
+ types: toYieldTypesParam(key.category),
+ yieldIds: ids,
+ },
+ }),
+ getItemId: (yieldDto) => yieldDto.id,
+ ids: key.yieldIds,
+ pageSize: DEFAULT_PAGE_SIZE,
+ });
+
+ return new Set(yields.map((yieldDto) => yieldDto.id));
+ }).pipe(withCatalogError("token-yield-scope"))
+ )
+ .pipe(catalogSWR)
+);
+
+const getTokenOptionRank = (token: EarnTokenOption) => {
+ if (token.source === "balance") {
+ return new BigNumber(token.amount).isGreaterThan(0) ? 0 : 2;
+ }
+
+ if (token.source === "init") {
+ return 1;
+ }
+
+ return 3;
+};
+
+const mergeTokenOptions = ({
+ balanceItems,
+ defaultItems,
+ initItems,
+}: {
+ balanceItems: ReadonlyArray;
+ defaultItems: ReadonlyArray;
+ initItems: ReadonlyArray;
+}) => {
+ const mergeAvailableYields = (
+ prev: EarnTokenOption | undefined,
+ next: EarnTokenOption
+ ) => [
+ ...new Set([...(prev?.availableYields ?? []), ...next.availableYields]),
+ ];
+
+ const mergeOption = (
+ prev: { option: EarnTokenOption } | undefined,
+ next: EarnTokenOption
+ ): EarnTokenOption => ({
+ ...next,
+ availableYields: mergeAvailableYields(prev?.option, next),
+ });
+
+ const byKey = new Map<
+ string,
+ {
+ balanceIndex: number | null;
+ defaultIndex: number | null;
+ initIndex: number | null;
+ option: EarnTokenOption;
+ }
+ >();
+
+ defaultItems.forEach((option, defaultIndex) => {
+ byKey.set(tokenString(option.token), {
+ balanceIndex: null,
+ defaultIndex,
+ initIndex: null,
+ option: mergeOption(undefined, option),
+ });
+ });
+
+ initItems.forEach((option, initIndex) => {
+ const key = tokenString(option.token);
+ const prev = byKey.get(key);
+
+ byKey.set(key, {
+ balanceIndex: null,
+ defaultIndex: prev?.defaultIndex ?? null,
+ initIndex,
+ option: mergeOption(prev, option),
+ });
+ });
+
+ balanceItems.forEach((option, balanceIndex) => {
+ const key = tokenString(option.token);
+ const prev = byKey.get(key);
+
+ byKey.set(key, {
+ balanceIndex,
+ defaultIndex: prev?.defaultIndex ?? null,
+ initIndex: prev?.initIndex ?? null,
+ option: mergeOption(prev, option),
+ });
+ });
+
+ return [...byKey.values()]
+ .sort((a, b) => {
+ const rankDiff =
+ getTokenOptionRank(a.option) - getTokenOptionRank(b.option);
+
+ if (rankDiff !== 0) {
+ return rankDiff;
+ }
+
+ return (
+ (a.balanceIndex ??
+ a.initIndex ??
+ a.defaultIndex ??
+ Number.MAX_SAFE_INTEGER) -
+ (b.balanceIndex ??
+ b.initIndex ??
+ b.defaultIndex ??
+ Number.MAX_SAFE_INTEGER)
+ );
+ })
+ .map(({ option }) => option);
+};
+
+export const mergedTokenOptionsAtom = Atom.family((key: TokenOptionsKey) => {
+ const defaultTokenOptionsAtom = defaultTokenOptionsPullAtom(
+ new DefaultTokenOptionsKey({
+ category: key.category,
+ network: key.network,
+ tokensForEnabledYieldsOnly: key.tokensForEnabledYieldsOnly,
+ })
+ );
+ const tokenBalancesAtom = tokenBalancesScanAtom(
+ new TokenBalancesScanKey({
+ address: key.address,
+ additionalAddresses: key.additionalAddresses,
+ network: key.network,
+ })
+ );
+ const initTokenAtom = initTokenOptionAtom(
+ new InitTokenOptionKey({
+ token: key.initToken,
+ network: key.initTokenNetwork,
+ })
+ );
+ const initYieldAtomValue = initYieldAtom(
+ new InitYieldKey({ yieldId: key.initYieldId })
+ );
+
+ return Atom.readable((context) => {
+ const defaultResult = context.get(defaultTokenOptionsAtom);
+ const balancesResult = context.get(tokenBalancesAtom);
+ const initTokenResult = context.get(initTokenAtom);
+ const initYieldResult = context.get(initYieldAtomValue);
+
+ const tokenSourcesResult = AsyncResult.all({
+ defaultItems: getPullItemsResult(defaultResult),
+ balanceItems: balancesResult,
+ initToken: initTokenResult,
+ initYield: initYieldResult,
+ });
+
+ return AsyncResult.flatMap(tokenSourcesResult, (sources, sourcesResult) => {
+ const rawInitItems = [
+ sources.initYield ? toInitYieldTokenOption(sources.initYield) : null,
+ sources.initToken,
+ ].filter((option): option is EarnTokenOption => option !== null);
+ const candidateYieldIds = getAvailableYieldIds([
+ ...sources.balanceItems,
+ ...rawInitItems,
+ ]);
+ const tokenYieldScopeResult: AsyncResult.AsyncResult<
+ ReadonlySet | null,
+ EarnCatalogError
+ > =
+ key.category && candidateYieldIds.length > 0
+ ? context.get(
+ tokenYieldScopeAtom(
+ new TokenYieldScopeKey({
+ category: key.category,
+ yieldIds: candidateYieldIds,
+ })
+ )
+ )
+ : AsyncResult.success(key.category ? new Set() : null);
+ const scopedResult = tokenYieldScopeResult.pipe(
+ AsyncResult.map((scopedYieldIds) =>
+ mergeTokenOptions({
+ balanceItems: scopeTokenOptions({
+ items: sources.balanceItems,
+ yieldIds: scopedYieldIds,
+ }),
+ defaultItems: sources.defaultItems.filter(hasAvailableYields),
+ initItems: scopeTokenOptions({
+ items: rawInitItems,
+ yieldIds: scopedYieldIds,
+ }),
+ })
+ )
+ );
+
+ return sourcesResult.waiting
+ ? AsyncResult.waiting(scopedResult)
+ : scopedResult;
+ });
+ });
+});
+
+export const tokenOptionsPullAtom = defaultTokenOptionsPullAtom;
+
+export const yieldValidatorsAtom = Atom.family(
+ ({ selectedYieldId }: YieldValidatorsKey) => {
+ const preferredValidatorsAtom = widgetAtomRuntime
+ .atom(() =>
+ Effect.gen(function* () {
+ const api = yield* StakeKitApiService;
+
+ const validators = yield* loadAllPages({
+ concurrency: PREFERRED_PAGE_CONCURRENCY,
+ fetchPage: (offset: number) =>
+ api.yield.YieldsControllerGetYieldValidators(selectedYieldId, {
+ params: {
+ limit: DEFAULT_PAGE_SIZE,
+ offset,
+ preferred: true,
+ status: "active",
+ },
+ }),
+ pageSize: DEFAULT_PAGE_SIZE,
+ });
+
+ return toValidators(validators);
+ }).pipe(withCatalogError("preferred-validators"))
+ )
+ .pipe(catalogSWR);
+
+ const loadedValidatorsAtom = Atom.writable<
+ Map,
+ ReadonlyArray
+ >(
+ (context) => {
+ const preferredValidators = context.get(preferredValidatorsAtom).pipe(
+ AsyncResult.value,
+ Option.getOrElse(() => [])
+ );
+ const loadedValidators = new Map(
+ context
+ .self