diff --git a/package.json b/package.json index 91dda17c..6b802fe2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "format": "turbo run format", "clean": "turbo run clean", "prepare": "husky", - "check-hygiene": "rev-dep config run --list-all-issues" + "check-hygiene": "rev-dep config run --list-all-issues", + "check": "turbo run lint && pnpm run check-hygiene && turbo run test && turbo run build" }, "devDependencies": { "@biomejs/biome": "catalog:", diff --git a/packages/widget/src/common/get-enabled-networks.ts b/packages/widget/src/common/get-enabled-networks.ts index ece15507..088ed424 100644 --- a/packages/widget/src/common/get-enabled-networks.ts +++ b/packages/widget/src/common/get-enabled-networks.ts @@ -17,8 +17,8 @@ export const getEnabledNetworks = ({ queryKey: [config.appPrefix, "enabled-networks"], queryFn: async () => new Set( - (await apiClient.yield.NetworksControllerGetNetworks(undefined)).map( - (network) => network.id as Networks + (await apiClient.legacy.YieldControllerGetMyNetworks(undefined)).map( + (network) => network as Networks ) ), }) diff --git a/packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx b/packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx index 6f3ce768..2623435e 100644 --- a/packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx +++ b/packages/widget/src/components/molecules/select-opportunity-list-item/index.tsx @@ -1,6 +1,6 @@ import type { ComponentProps } from "react"; import { useTranslation } from "react-i18next"; -import type { Yield } from "../../../domain/types/yields"; +import type { YieldBase } from "../../../domain/types/yields"; import { capitalizeFirstLetters, getRewardRateFormatted, @@ -17,17 +17,19 @@ import { selectItemText, } from "./styles.css"; -export const SelectOpportunityListItem = ({ +type SelectOpportunityListItemProps = { + item: T; + onYieldSelect: (item: T) => void; + testId?: string; + selected?: boolean; +}; + +export const SelectOpportunityListItem = ({ item, onYieldSelect, testId, selected, -}: { - item: Yield; - onYieldSelect: (item: Yield) => void; - testId?: string; - selected?: boolean; -}) => { +}: SelectOpportunityListItemProps) => { const { t } = useTranslation(); const onItemClick: ComponentProps["onItemClick"] = ({ diff --git a/packages/widget/src/domain/types/stake.ts b/packages/widget/src/domain/types/stake.ts index 178910a1..748cbd08 100644 --- a/packages/widget/src/domain/types/stake.ts +++ b/packages/widget/src/domain/types/stake.ts @@ -8,7 +8,12 @@ import type { InitParams } from "./init-params"; import type { PositionsData } from "./positions"; import type { TokenBalanceScanResponseDto } from "./token-balance"; import type { TokenString } from "./tokens"; -import { getYieldActionArg, isBittensorStaking, type Yield } from "./yields"; +import { + getYieldActionArg, + isBittensorStaking, + type Yield, + type YieldBase, +} from "./yields"; const amountGreaterThanZero = (val: TokenBalanceScanResponseDto) => new BigNumber(val.amount).isGreaterThan(0); @@ -87,7 +92,7 @@ export const getInitialToken = (args: { export const canBeInitialYield = (args: { initQueryParams: Maybe; - yieldDto: Yield; + yieldDto: YieldBase; tokenBalanceAmount: BigNumber; positionsData: PositionsData; }) => { @@ -113,7 +118,7 @@ const balanceValidForYield = ({ positionsData, }: { tokenBalanceAmount: BigNumber; - yieldDto: Yield; + yieldDto: YieldBase; positionsData: PositionsData; }) => tokenBalanceAmount.isGreaterThanOrEqualTo( @@ -149,7 +154,7 @@ const yieldsWithEnterMinBasedOnPosition = new Map>([ export const isNetworkWithEnterMinBasedOnPosition = (network: Networks) => yieldsWithEnterMinBasedOnPosition.has(network); -const isYieldWithEnterMinBasedOnPosition = (yieldDto: Yield) => +const isYieldWithEnterMinBasedOnPosition = (yieldDto: YieldBase) => Maybe.fromNullable( yieldsWithEnterMinBasedOnPosition.get( yieldDto.mechanics.gasFeeToken.network as Networks @@ -159,7 +164,7 @@ const isYieldWithEnterMinBasedOnPosition = (yieldDto: Yield) => .isJust(); export const getMinStakeAmount = ( - yieldDto: Yield, + yieldDto: YieldBase, positionsData: PositionsData ) => { const integrationMin = new BigNumber( diff --git a/packages/widget/src/domain/types/yields.ts b/packages/widget/src/domain/types/yields.ts index 96d55516..98590e46 100644 --- a/packages/widget/src/domain/types/yields.ts +++ b/packages/widget/src/domain/types/yields.ts @@ -2,7 +2,6 @@ import BigNumber from "bignumber.js"; import { Array as EArray, pipe } from "effect"; import type { TFunction } from "i18next"; import { Maybe } from "purify-ts"; -import type { YieldDto as OldYieldDto } from "../../generated/api/legacy"; import type { YieldType as ApiYieldType, ArgumentFieldDto, @@ -15,12 +14,14 @@ import type { SupportedSKChains } from "./chains"; import { EvmNetworks } from "./chains/networks"; import { equalTokens, tokenString } from "./tokens"; +export type YieldProviderDetails = ProviderDto; + export type Yield = YieldApiYieldDto & { - __fallback__: OldYieldDto; provider?: YieldProviderDetails; }; -export type YieldProviderDetails = ProviderDto; +export type YieldBase = Yield; + type YieldRiskRatingTone = "positive" | "warning" | "danger" | "neutral"; type KnownYieldRiskRatingSource = YieldRiskEntryDto["source"]; type YieldRiskRatingSource = KnownYieldRiskRatingSource | (string & {}); @@ -110,7 +111,7 @@ export const getDashboardYieldCategoryForApiYieldType = ( ): DashboardYieldCategory => apiYieldTypeToDashboardCategory[yieldType]; export const getDashboardYieldCategory = ( - yieldDto: Yield + yieldDto: YieldBase ): DashboardYieldCategory | null => { const yieldType = getExtendedYieldType(yieldDto); @@ -192,7 +193,7 @@ const secondsToDays = (seconds: number | undefined) => { }; export const getYieldActionArg = ( - yieldDto: Yield, + yieldDto: YieldBase, type: YieldActionType, name: YieldArgumentName ): YieldArgumentConfig | null => { @@ -213,12 +214,12 @@ export const getYieldActionArg = ( }; export const isYieldActionArgRequired = ( - yieldDto: Yield, + yieldDto: YieldBase, type: YieldActionType, name: YieldArgumentName ) => !!getYieldActionArg(yieldDto, type, name)?.required; -export const getYieldRewardTokens = (yieldDto: Yield) => +export const getYieldRewardTokens = (yieldDto: YieldBase) => pipe( yieldDto.rewardRate?.components?.map((component) => component.token) ?? [], EArray.dedupeWith((a, b) => tokenString(a) === tokenString(b)), @@ -277,9 +278,6 @@ export const getYieldCooldownPeriod = (yieldDto: Yield) => export const getYieldWarmupPeriod = (yieldDto: Yield) => secondsToDays(yieldDto.mechanics.warmupPeriod?.seconds); -export const getYieldCommission = (yieldDto: Yield) => - yieldDto.__fallback__.metadata.commission; - export const getYieldTvlUsd = (yieldDto: Yield) => { const tvlUsd = yieldDto.statistics?.tvlUsd; @@ -307,10 +305,9 @@ export const getYieldFeePercent = (yieldDto: Yield): number | null => { export const getYieldLockupPeriod = (yieldDto: Yield) => secondsToDays(yieldDto.mechanics.lockupPeriod?.seconds); -export const hasYieldExitSignatureVerification = (yieldDto: Yield) => - !!yieldDto.__fallback__.args.exit?.args?.signatureVerification?.required; - -export const getExtendedYieldType = (yieldDto: Yield): ExtendedYieldType => { +export const getExtendedYieldType = ( + yieldDto: YieldBase +): ExtendedYieldType => { if (isNativeStaking(yieldDto)) { return "native_staking"; } @@ -322,7 +319,7 @@ export const getExtendedYieldType = (yieldDto: Yield): ExtendedYieldType => { return yieldDto.mechanics.type; }; -export const getYieldOutputToken = (yieldDto: Yield) => +export const getYieldOutputToken = (yieldDto: YieldBase) => Maybe.fromNullable(yieldDto.outputToken).filter( (outputToken) => !equalTokens(outputToken, yieldDto.token) ); @@ -344,7 +341,7 @@ export const isDepositYieldType = (yieldType: ExtendedYieldType) => yieldType === "liquidity_pool"; export const getYieldTypeLabels = ( - yieldDto: Yield, + yieldDto: YieldBase, t: TFunction ): YieldTypeLabelsMap[keyof YieldTypeLabelsMap] => { const map = { @@ -426,15 +423,15 @@ const yieldTypesSortRank: { [Key in ExtendedYieldType]: number } = { concentrated_liquidity_pool: 10, }; -export const getYieldTypesSortRank = (yieldDto: Yield) => +export const getYieldTypesSortRank = (yieldDto: YieldBase) => yieldTypesSortRank[getExtendedYieldType(yieldDto)]; -const isEthereumStaking = (yieldDto: Yield) => +const isEthereumStaking = (yieldDto: YieldBase) => yieldDto.mechanics.type === "staking" && yieldDto.token.network === EvmNetworks.Ethereum && yieldDto.token.symbol === "ETH"; -const isNativeStaking = (yieldDto: Yield) => +const isNativeStaking = (yieldDto: YieldBase) => Maybe.fromFalsy(isEthereumStaking(yieldDto)) .chain(() => Maybe.fromFalsy( @@ -449,22 +446,18 @@ const isNativeStaking = (yieldDto: Yield) => .filter((v) => v.isEqualTo(32)) .isJust(); -const isPooledStaking = (yieldDto: Yield) => +const isPooledStaking = (yieldDto: YieldBase) => isEthereumStaking(yieldDto) && !isNativeStaking(yieldDto); -export const isYieldWithProviderOptions = (yieldDto: Yield) => +export const isYieldWithProviderOptions = (yieldDto: YieldBase) => !!getYieldActionArg(yieldDto, "enter", "providerId")?.required; -export const getYieldProviderYieldIds = (yieldDto: Yield) => +export const getYieldProviderYieldIds = (yieldDto: YieldBase) => getYieldActionArg(yieldDto, "enter", "providerId")?.options ?? []; -export const isYieldIntegrationAggregator = (yieldDto: Yield) => - !!yieldDto.__fallback__.metadata.isIntegrationAggregator; - export const isYieldValidatorSelectionRequired = (yieldDto: Yield) => !!( yieldDto.mechanics.requiresValidatorSelection || - isYieldIntegrationAggregator(yieldDto) || isYieldActionArgRequired(yieldDto, "enter", "validatorAddress") || isYieldActionArgRequired(yieldDto, "enter", "validatorAddresses") ); diff --git a/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts b/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts index 451c140d..6c5e0817 100644 --- a/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts +++ b/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts @@ -14,10 +14,12 @@ import { getYieldSummariesQueryKey, isVisibleYieldSummary, type YieldSummariesParams, + type YieldSummary, } from "./use-yield-summaries"; type DashboardCategoryInitialSelection = { token: TokenDto; + yieldDto: YieldSummary; yieldId: string; }; @@ -31,18 +33,21 @@ const staleTime = 1000 * 60 * 2; */ export const useDashboardYieldCatalog = ({ enabled = true, + network, }: { enabled?: boolean; + network?: TokenDto["network"] | null; } = {}) => { - const { network } = useSKWallet(); + const { network: walletNetwork } = useSKWallet(); const apiClient = useApiClient(); - const probeEnabled = enabled && !!network; + const catalogNetwork = network === null ? null : (network ?? walletNetwork); + const probeEnabled = enabled && (network === null || !!catalogNetwork); const results = useQueries({ queries: dashboardYieldCategories.map((category) => { const params: YieldSummariesParams = { - network: network ?? undefined, + ...(catalogNetwork ? { network: catalogNetwork } : {}), types: getApiYieldTypesForDashboardCategory(category), sort: "rewardRateDesc", limit: DEFAULT_YIELD_SUMMARIES_PAGE_LIMIT, @@ -75,6 +80,7 @@ export const useDashboardYieldCatalog = ({ availableCategories.push(category); initialSelectionByCategory.set(category, { token: firstVisible.token, + yieldDto: firstVisible, yieldId: firstVisible.id, }); }); diff --git a/packages/widget/src/hooks/api/use-multi-yields.ts b/packages/widget/src/hooks/api/use-multi-yields.ts index 954be0c5..cfb95121 100644 --- a/packages/widget/src/hooks/api/use-multi-yields.ts +++ b/packages/widget/src/hooks/api/use-multi-yields.ts @@ -14,6 +14,7 @@ import { map, mergeMap, Observable, + of, repeat, take, tap, @@ -36,20 +37,28 @@ import { isNonZeroRewardRateYield, type ValidatorsConfig, type Yield, + type 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 } from "./use-yield-opportunity/get-yield-opportunity"; +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>() }, + context: { data: new Map>() }, on: { "yield-opportunity": ( context, - event: { data: { key: string; yieldDto: Yield } } + event: { data: { key: string; yieldDto: YieldSummaryWithProvider } } ) => { const newMap = new Map(context.data); const prev = newMap.get(event.data.key) ?? new Map(); @@ -62,7 +71,7 @@ const multiYieldsStore = createStore({ }, }); -export const useStreamMultiYields = (yieldIds: ReadonlyArray) => { +export const useStreamYieldSummaries = (yieldIds: ReadonlyArray) => { const { network, isConnected, isLedgerLive } = useSKWallet(); const apiClient = useApiClient(); @@ -79,7 +88,7 @@ export const useStreamMultiYields = (yieldIds: ReadonlyArray) => { const validatorsConfig = useValidatorsConfig(); useEffect(() => { - const sub = multipleYields$({ + const sub = multipleYieldSummaries$({ ...argsRef.current, yieldIds, validatorsConfig, @@ -186,6 +195,39 @@ 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; @@ -199,9 +241,9 @@ const firstEligibleYield$ = (args: { validatorsConfig: ValidatorsConfig; preferredTokenYieldsPerNetwork: PreferredTokenYieldsPerNetwork | null; }) => { - let defaultYield: Yield | null = null; + let defaultYield: YieldSummaryWithProvider | null = null; - const successStream = multipleYields$(args).pipe( + const successStream = multipleYieldSummaries$(args).pipe( tap((v) => { if (isNonZeroRewardRateYield(v) || !defaultYield) { defaultYield = v; @@ -232,20 +274,36 @@ const firstEligibleYield$ = (args: { }); }), take(1), - defaultIfEmpty(null) + 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 ?? defaultYield), + next: (v) => subscriber.next(v), error: (e) => subscriber.error(e), }); }); }; type SelectorInputData = { - data: Yield[]; + data: YieldBase[]; isConnected: boolean; network: SKWallet["network"]; isLedgerLive: boolean; diff --git a/packages/widget/src/hooks/api/use-token-list-yields.ts b/packages/widget/src/hooks/api/use-token-list-yields.ts index f88468da..3f6c9439 100644 --- a/packages/widget/src/hooks/api/use-token-list-yields.ts +++ b/packages/widget/src/hooks/api/use-token-list-yields.ts @@ -1,13 +1,10 @@ -import { useQuery } from "@tanstack/react-query"; -import { useMemo } from "react"; import type { TokenBalanceScanResponseDto } from "../../domain/types/token-balance"; -import { tokenString } from "../../domain/types/tokens"; import { type DashboardYieldCategory, getDashboardYieldCategoryForApiYieldType, } from "../../domain/types/yields"; import type { YieldDto } from "../../generated/api/yield"; -import { useApiClient } from "../../providers/api/api-client-provider"; +import type { useApiClient } from "../../providers/api/api-client-provider"; import { getRewardRateFormatted, getRewardTypeFormatted, @@ -17,9 +14,7 @@ import { isVisibleYieldSummary, } from "./use-yield-summaries"; -const staleTime = 1000 * 60 * 2; - -export type TokenMaxYieldRate = { +type TokenMaxYieldRate = { rateFormatted: string; rateTypeLabel: string; }; @@ -100,83 +95,3 @@ export const getMaxYieldRateForToken = ( rateTypeLabel, }; }; - -export const useTokenListYields = ( - tokenBalances: ReadonlyArray, - dashboardYieldCategory?: DashboardYieldCategory | null -) => { - const apiClient = useApiClient(); - const yieldIds = getUniqueYieldIds(tokenBalances); - - const query = useQuery({ - queryKey: ["token-list-yields", yieldIds], - enabled: yieldIds.length > 0, - staleTime, - queryFn: async ({ signal }) => - fetchTokenListYieldSummaries({ - apiClient, - signal, - tokenBalances, - }), - }); - - const yieldsById = useMemo( - () => - new Map((query.data ?? []).map((yieldDto) => [yieldDto.id, yieldDto])), - [query.data] - ); - - const yieldIdsByToken = useMemo(() => { - const map = new Map(); - - for (const tokenBalance of tokenBalances) { - map.set( - tokenString(tokenBalance.token), - dashboardYieldCategory - ? getDashboardCategoryYieldIdsForToken( - tokenBalance.availableYields, - yieldsById, - dashboardYieldCategory - ) - : [...tokenBalance.availableYields] - ); - } - - return map; - }, [dashboardYieldCategory, tokenBalances, yieldsById]); - - const yieldCountsByToken = useMemo( - () => - new Map( - [...yieldIdsByToken.entries()].map(([token, tokenYieldIds]) => [ - token, - tokenYieldIds.length, - ]) - ), - [yieldIdsByToken] - ); - - const maxYieldRatesByToken = useMemo(() => { - const map = new Map(); - - for (const tokenBalance of tokenBalances) { - const maxYieldRate = getMaxYieldRateForToken( - yieldIdsByToken.get(tokenString(tokenBalance.token)) ?? [], - yieldsById - ); - - if (maxYieldRate) { - map.set(tokenString(tokenBalance.token), maxYieldRate); - } - } - - return map; - }, [tokenBalances, yieldIdsByToken, yieldsById]); - - return { - yieldIdsByToken, - yieldCountsByToken, - maxYieldRatesByToken, - isLoading: query.isLoading, - }; -}; 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 0a7aba70..058614f7 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 @@ -3,10 +3,12 @@ import { EitherAsync } from "purify-ts"; import { isEthenaUsdeStaking, type Yield, + type YieldBase, type YieldProviderDetails, } from "../../../domain/types/yields"; import type { ApiClient } from "../../../providers/api/api-client"; -import { fetchYieldSummariesByIds } from "../use-yield-summaries"; +import { fetchYieldProvider } from "../use-yield-providers"; +import { fetchYieldSummariesWithProvidersByIds } from "../use-yield-summaries"; type Params = { yieldId: string; @@ -22,10 +24,12 @@ type MultiParams = Omit & { type ParamsWithQueryClient = Params & { queryClient: QueryClient; }; +type ParamsWithYieldDto = Omit & { + yieldDto: YieldBase; +}; type MultiParamsWithQueryClient = MultiParams & { queryClient: QueryClient; }; -type YieldApiYield = Omit; const staleTime = 1000 * 60 * 2; const getKey = (params: Params) => [ @@ -38,7 +42,6 @@ const getMultiKey = (params: MultiParams) => [ params.yieldIds, params.isLedgerLive, ]; -const getProviderKey = (providerId: string) => ["yield-provider", providerId]; const applyYieldOverrides = (yieldDto: Yield) => isEthenaUsdeStaking(yieldDto.id) @@ -52,65 +55,17 @@ const applyYieldOverrides = (yieldDto: Yield) => : yieldDto; const createYield = ({ - legacyYield, provider, yieldDto, }: { - legacyYield: Yield["__fallback__"]; provider: YieldProviderDetails | undefined; - yieldDto: YieldApiYield; + yieldDto: YieldBase; }) => ({ ...yieldDto, ...(provider ? { provider } : {}), - __fallback__: legacyYield, }) satisfies Yield; -const fetchYieldProvider = async ({ - client, - providerId, - queryClient, -}: { - client: ReturnType; - providerId: string; - queryClient: QueryClient; -}): Promise => { - try { - return await queryClient.fetchQuery({ - queryKey: getProviderKey(providerId), - staleTime, - queryFn: () => - client.yield.ProvidersControllerGetProvider(providerId, undefined), - }); - } catch (e) { - console.log(e); - return undefined; - } -}; - -const fetchYieldProviders = async ({ - client, - providerIds, - queryClient, -}: { - client: ReturnType; - providerIds: ReadonlyArray; - queryClient: QueryClient; -}) => { - const providers = await Promise.all( - [...new Set(providerIds)].map(async (providerId) => ({ - providerId, - provider: await fetchYieldProvider({ client, providerId, queryClient }), - })) - ); - - return new Map( - providers.flatMap(({ provider, providerId }) => - provider ? [[providerId, provider]] : [] - ) - ); -}; - export const getYieldOpportunity = (params: ParamsWithQueryClient) => EitherAsync(() => params.queryClient.fetchQuery({ @@ -123,6 +78,18 @@ 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({ @@ -172,7 +139,6 @@ const multiQueryFn = async ( ) => (await multiFn(params)).unsafeCoerce(); const fn = ({ - isLedgerLive, yieldId, queryClient, signal, @@ -183,12 +149,10 @@ const fn = ({ }) => { return EitherAsync(async () => { const client = apiClient.withOptions({ signal, suppressRichErrors }); - const [newYieldResult, legacyYieldResult] = await Promise.all([ - client.yield.YieldsControllerGetYield(yieldId, undefined), - client.legacy.YieldControllerYieldOpportunity(yieldId, { - params: { ledgerWalletAPICompatible: isLedgerLive }, - }), - ]); + const newYieldResult = await client.yield.YieldsControllerGetYield( + yieldId, + undefined + ); const provider = await fetchYieldProvider({ client, providerId: newYieldResult.providerId, @@ -196,7 +160,6 @@ const fn = ({ }); return createYield({ - legacyYield: legacyYieldResult, provider, yieldDto: newYieldResult, }); @@ -208,8 +171,33 @@ 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 = ({ - isLedgerLive, queryClient, yieldIds, signal, @@ -219,53 +207,22 @@ const multiFn = ({ signal?: AbortSignal; }) => { return EitherAsync(async () => { - const client = apiClient.withOptions({ signal, suppressRichErrors }); - const newYields = await fetchYieldSummariesByIds({ + const yields = await fetchYieldSummariesWithProvidersByIds({ apiClient, + queryClient, signal, suppressRichErrors, yieldIds, }); - const newYieldsById = new Map( - newYields.map((yieldDto) => [yieldDto.id, yieldDto]) - ); - const providersById = await fetchYieldProviders({ - client, - providerIds: [...newYieldsById.values()].map( - (yieldDto) => yieldDto.providerId - ), - queryClient, - }); - - const yields = await Promise.all( - yieldIds.map(async (yieldId) => { - const newYieldResult = newYieldsById.get(yieldId); - if (!newYieldResult) { - return null; - } - - try { - const legacyYieldResult = - await client.legacy.YieldControllerYieldOpportunity(yieldId, { - params: { ledgerWalletAPICompatible: isLedgerLive }, - }); - - return applyYieldOverrides( - createYield({ - legacyYield: legacyYieldResult, - provider: providersById.get(newYieldResult.providerId), - yieldDto: newYieldResult, - }) - ); - } catch (e) { - console.log(e); - return null; - } - }) + return yields.map((yieldDto) => + applyYieldOverrides( + createYield({ + provider: yieldDto.provider, + yieldDto, + }) + ) ); - - return yields.filter((yieldDto) => yieldDto !== null); }).mapLeft((e) => { console.log(e); return new Error("Could not get yield opportunities"); diff --git a/packages/widget/src/hooks/api/use-yield-providers.ts b/packages/widget/src/hooks/api/use-yield-providers.ts new file mode 100644 index 00000000..4faab21c --- /dev/null +++ b/packages/widget/src/hooks/api/use-yield-providers.ts @@ -0,0 +1,51 @@ +import type { QueryClient } from "@tanstack/react-query"; +import type { YieldProviderDetails } from "../../domain/types/yields"; +import type { ApiClient } from "../../providers/api/api-client"; + +const staleTime = 1000 * 60 * 2; +const getProviderKey = (providerId: string) => ["yield-provider", providerId]; + +export const fetchYieldProvider = async ({ + client, + providerId, + queryClient, +}: { + client: ReturnType; + providerId: string; + queryClient: QueryClient; +}): Promise => { + try { + return await queryClient.fetchQuery({ + queryKey: getProviderKey(providerId), + staleTime, + queryFn: () => + client.yield.ProvidersControllerGetProvider(providerId, undefined), + }); + } catch (e) { + console.log(e); + return undefined; + } +}; + +export const fetchYieldProviders = async ({ + client, + providerIds, + queryClient, +}: { + client: ReturnType; + providerIds: ReadonlyArray; + queryClient: QueryClient; +}) => { + const providers = await Promise.all( + [...new Set(providerIds)].map(async (providerId) => ({ + providerId, + provider: await fetchYieldProvider({ client, providerId, queryClient }), + })) + ); + + return new Map( + providers.flatMap(({ provider, providerId }) => + provider ? [[providerId, provider]] : [] + ) + ); +}; diff --git a/packages/widget/src/hooks/api/use-yield-summaries.ts b/packages/widget/src/hooks/api/use-yield-summaries.ts index 9c6b8a2b..3fa4dc23 100644 --- a/packages/widget/src/hooks/api/use-yield-summaries.ts +++ b/packages/widget/src/hooks/api/use-yield-summaries.ts @@ -1,19 +1,27 @@ +import type { QueryClient } from "@tanstack/react-query"; import { isSupportedChain } from "../../domain/types/chains"; -import { isNonZeroRewardRateYield } from "../../domain/types/yields"; +import { + isEthenaUsdeStaking, + isNonZeroRewardRateYield, + type YieldProviderDetails, +} from "../../domain/types/yields"; import type { YieldDto, YieldsControllerGetYieldsParams, } from "../../generated/api/yield"; import type { useApiClient } from "../../providers/api/api-client-provider"; +import { fetchYieldProviders } from "./use-yield-providers"; /** * A yield "summary" is the new yields API DTO, returned by * `YieldsControllerGetYields`. It contains everything category discovery and * list rendering need (`token`, `rewardRate`, `status`, `metadata`, - * `mechanics.type`, `providerId`) and intentionally does NOT include the legacy - * `__fallback__` hydration, which is only required for a selected yield. + * `mechanics.type`, `providerId`). */ export type YieldSummary = YieldDto; +export type YieldSummaryWithProvider = YieldSummary & { + provider?: YieldProviderDetails; +}; export const DEFAULT_YIELD_SUMMARIES_PAGE_LIMIT = 50; const DEFAULT_YIELD_IDS_CHUNK_SIZE = 100; @@ -50,6 +58,10 @@ type FetchByIdsArgs = { yieldIds: ReadonlyArray; }; +type FetchByIdsWithProvidersArgs = FetchByIdsArgs & { + queryClient: QueryClient; +}; + const unique = (items: ReadonlyArray) => [...new Set(items)]; const chunks = (items: ReadonlyArray, chunkSize: number): T[][] => { @@ -118,6 +130,47 @@ export const fetchYieldSummariesByIds = async ({ }); }; +const applyYieldSummaryOverrides = (yieldDto: T): T => + isEthenaUsdeStaking(yieldDto.id) + ? ({ + ...yieldDto, + metadata: { + ...yieldDto.metadata, + name: yieldDto.metadata.name.replace(/staking/i, ""), + }, + } as T) + : yieldDto; + +export const fetchYieldSummariesWithProvidersByIds = async ({ + apiClient, + queryClient, + signal, + suppressRichErrors, + yieldIds, +}: FetchByIdsWithProvidersArgs): Promise => { + const client = apiClient.withOptions({ signal, suppressRichErrors }); + const summaries = await fetchYieldSummariesByIds({ + apiClient, + signal, + suppressRichErrors, + yieldIds, + }); + const providersById = await fetchYieldProviders({ + client, + providerIds: summaries.map((yieldDto) => yieldDto.providerId), + queryClient, + }); + + return summaries.map((summary) => { + const provider = providersById.get(summary.providerId); + + return applyYieldSummaryOverrides({ + ...summary, + ...(provider ? { provider } : {}), + }); + }); +}; + /** * Fetch every page of yield summaries for the given params, looping `offset` * until all `total` items have been retrieved. 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 5fe1c854..c57e6e3e 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 @@ -1,7 +1,6 @@ import BigNumber from "bignumber.js"; import type { ComponentProps } from "react"; import { memo } from "react"; -import { useTranslation } from "react-i18next"; import { Box } from "../../../../../components/atoms/box"; import { SelectModalItem, @@ -10,40 +9,24 @@ import { import { TokenIcon } from "../../../../../components/atoms/token-icon"; import { Text } from "../../../../../components/atoms/typography/text"; import type { TokenBalanceScanResponseDto } from "../../../../../domain/types/token-balance"; -import type { TokenMaxYieldRate } from "../../../../../hooks/api/use-token-list-yields"; import { useTrackEvent } from "../../../../../hooks/tracking/use-track-event"; import { selectItemText } from "../../styles.css"; -import { maxYieldRateLabel, maxYieldRateText } from "./styles.css"; type Props = { item: TokenBalanceScanResponseDto; isConnected: boolean; isSelected: boolean; - availableYieldsCount?: number; - canSelectToken?: boolean; - maxYieldRate?: TokenMaxYieldRate; onTokenBalanceSelect: (tokenBalance: TokenBalanceScanResponseDto) => void; }; export const SelectTokenListItem = memo( - ({ - item, - isConnected, - isSelected, - availableYieldsCount = item.availableYields.length, - canSelectToken = true, - maxYieldRate, - onTokenBalanceSelect, - }: Props) => { + ({ item, isConnected, isSelected, onTokenBalanceSelect }: Props) => { const amountGreaterThanZero = new BigNumber(item.amount).isGreaterThan(0); const trackEvent = useTrackEvent(); - const { t } = useTranslation(); const _onItemClick: ComponentProps["onItemClick"] = ({ closeModal }) => { - if (!canSelectToken) return; - trackEvent("tokenSelected", { token: item.token.symbol }); onTokenBalanceSelect(item); closeModal(); @@ -54,11 +37,9 @@ export const SelectTokenListItem = memo( @@ -81,35 +62,9 @@ export const SelectTokenListItem = memo( - {t("select_token.yields_available", { - count: availableYieldsCount, - token_name: item.token.name, - })} + {item.token.name} - - {maxYieldRate ? ( - - - {maxYieldRate.rateFormatted} - - - - {t("select_token.up_to_rate_type", { - rateType: maxYieldRate.rateTypeLabel, - })} - - - ) : null} diff --git a/packages/widget/src/pages/details/earn-page/components/select-token-section/select-token.tsx b/packages/widget/src/pages/details/earn-page/components/select-token-section/select-token.tsx index ebfe38ea..f5c85d9e 100644 --- a/packages/widget/src/pages/details/earn-page/components/select-token-section/select-token.tsx +++ b/packages/widget/src/pages/details/earn-page/components/select-token-section/select-token.tsx @@ -12,8 +12,7 @@ import { SelectModal } from "../../../../../components/atoms/select-modal"; import { TokenIcon } from "../../../../../components/atoms/token-icon"; import { Text } from "../../../../../components/atoms/typography/text"; import { VirtualList } from "../../../../../components/atoms/virtual-list"; -import type { TokenBalanceScanResponseDto } from "../../../../../domain/types/token-balance"; -import { equalTokens, tokenString } from "../../../../../domain/types/tokens"; +import { equalTokens } from "../../../../../domain/types/tokens"; import { useTrackEvent } from "../../../../../hooks/tracking/use-track-event"; import { useSettings } from "../../../../../providers/settings"; import { useSKWallet } from "../../../../../providers/sk-wallet"; @@ -22,24 +21,6 @@ import { useEarnPageContext } from "../../state/earn-page-context"; import { validatorVirtuosoContainer } from "../../styles.css"; import { SelectTokenListItem } from "./select-token-list-item"; -const getAvailableYieldsCount = ({ - item, - selectedDashboardYieldCategory, - tokenListYieldsIsLoading, - tokenYieldCountsByToken, -}: { - item: TokenBalanceScanResponseDto; - selectedDashboardYieldCategory: unknown; - tokenListYieldsIsLoading: boolean; - tokenYieldCountsByToken: ReadonlyMap; -}) => { - if (!selectedDashboardYieldCategory || tokenListYieldsIsLoading) { - return item.availableYields.length; - } - - return tokenYieldCountsByToken.get(tokenString(item.token)) ?? 0; -}; - export const SelectToken = ({ canSelect = true }: { canSelect?: boolean }) => { const { onSelectTokenClose, @@ -48,10 +29,6 @@ export const SelectToken = ({ canSelect = true }: { canSelect?: boolean }) => { selectedToken, onTokenSearch, tokenSearch, - selectedDashboardYieldCategory, - tokenMaxYieldRatesByToken, - tokenYieldCountsByToken, - tokenListYieldsIsLoading, } = useEarnPageContext(); const { variant } = useSettings(); @@ -71,25 +48,11 @@ export const SelectToken = ({ canSelect = true }: { canSelect?: boolean }) => { return { st, - tokenBalances: tokenBalances.filter( - (item) => - getAvailableYieldsCount({ - item, - selectedDashboardYieldCategory, - tokenListYieldsIsLoading, - tokenYieldCountsByToken, - }) > 0 - ), + tokenBalances, }; }) .extractNullable(), - [ - selectedDashboardYieldCategory, - selectedToken, - tokenBalancesData, - tokenListYieldsIsLoading, - tokenYieldCountsByToken, - ] + [selectedToken, tokenBalancesData] ); if (!data) return null; @@ -123,7 +86,6 @@ export const SelectToken = ({ canSelect = true }: { canSelect?: boolean }) => { searchValue={tokenSearch} onClose={onSelectTokenClose} onOpen={() => trackEvent("selectTokenModalOpened")} - isLoading={!!selectedDashboardYieldCategory && tokenListYieldsIsLoading} trigger={ { data={data.tokenBalances} estimateSize={() => 60} itemContent={(_index, item) => { - const tokenKey = tokenString(item.token); - const availableYieldsCount = getAvailableYieldsCount({ - item, - selectedDashboardYieldCategory, - tokenListYieldsIsLoading, - tokenYieldCountsByToken, - }); - const canSelectToken = - !selectedDashboardYieldCategory || - (!tokenListYieldsIsLoading && availableYieldsCount > 0); - return ( diff --git a/packages/widget/src/pages/details/earn-page/components/select-token-section/styles.css.ts b/packages/widget/src/pages/details/earn-page/components/select-token-section/styles.css.ts index c66815f2..a596eac9 100644 --- a/packages/widget/src/pages/details/earn-page/components/select-token-section/styles.css.ts +++ b/packages/widget/src/pages/details/earn-page/components/select-token-section/styles.css.ts @@ -126,18 +126,6 @@ export const selectTokenBalance = recipe({ }, }); -export const maxYieldRateText = style([ - atoms({ color: "positionsRewardRate", fontWeight: "medium" }), - { whiteSpace: "nowrap" }, -]); - -export const maxYieldRateLabel = style([ - { - textTransform: "uppercase", - whiteSpace: "nowrap", - }, -]); - export const minMaxContainer = recipe({ base: [ atoms({ 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 186fde3f..9a2cbb3e 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 @@ -41,7 +41,11 @@ export const SelectYieldSection = () => { ) : ( selectedStakeData .map((val) => { - return val.all.length === 0 ? ( + const opportunityCount = dashboardVariant + ? val.filtered.length + : val.all.length; + + return opportunityCount === 0 ? ( availableYields.orDefault([]), [availableYields]) ); @@ -269,29 +275,39 @@ export const EarnPageContextProvider = ({ const dashboardYieldCatalog = useDashboardYieldCatalog({ enabled: dashboardVariant, + network: null, }); const availableDashboardYieldCategories = dashboardYieldCatalog.availableCategories; - const selectedDashboardYieldCategory = selectedStake + const selectedStakeDashboardYieldCategory = selectedStake .chainNullable(getDashboardYieldCategory) .extractNullable(); + const [ + selectedDashboardYieldCategoryFallback, + setSelectedDashboardYieldCategoryFallback, + ] = useState(null); + const selectedDashboardYieldCategory = + selectedStakeDashboardYieldCategory ?? + selectedDashboardYieldCategoryFallback; - const tokenBalancesForYieldMetadata = useMemo( - () => tokenBalancesData.map((v) => v.filtered).orDefault([]), - [tokenBalancesData] - ); + useEffect(() => { + if (!selectedStakeDashboardYieldCategory) return; - const tokenListYields = useTokenListYields( - tokenBalancesForYieldMetadata, - dashboardVariant ? selectedDashboardYieldCategory : null - ); + setSelectedDashboardYieldCategoryFallback( + selectedStakeDashboardYieldCategory + ); + }, [selectedStakeDashboardYieldCategory]); const dashboardSelectionByCategoryRef = useRef( new Map< DashboardYieldCategory, - { token: TokenBalanceScanResponseDto["token"]; yieldId: Yield["id"] } + { + token: TokenBalanceScanResponseDto["token"]; + yieldDto?: YieldBase; + yieldId: Yield["id"]; + } >() ); @@ -306,13 +322,22 @@ export const EarnPageContextProvider = ({ dashboardSelectionByCategoryRef.current.set(category, { token, + yieldDto, yieldId: yieldDto.id, }); }, [dashboardVariant, selectedStake, selectedToken]); const selectedStakeData = useMemo>( () => - Maybe.of(multiYields) + Maybe.of(yieldSummaries) + .map((val) => + selectedStake + .filter( + (stake) => !val.some((yieldDto) => yieldDto.id === stake.id) + ) + .map((stake) => [stake, ...val]) + .orDefault(val) + ) .map((val) => [...val].sort((a, b) => b.rewardRate.total - a.rewardRate.total) ) @@ -374,7 +399,7 @@ export const EarnPageContextProvider = ({ { type: ExtendedYieldType; title: ReturnType["title"]; - items: Yield[]; + items: YieldBase[]; } >() ) @@ -402,7 +427,8 @@ export const EarnPageContextProvider = ({ [ dashboardVariant, deferredStakeSearch, - multiYields, + selectedStake, + yieldSummaries, selectedDashboardYieldCategory, t, ] @@ -503,17 +529,28 @@ export const EarnPageContextProvider = ({ const selectDashboardTokenYield = useCallback( ({ token, + yieldDto, yieldId, }: { token: TokenBalanceScanResponseDto["token"]; + yieldDto?: YieldBase; yieldId: Yield["id"]; }) => { - getYieldOpportunity({ - yieldId, - isLedgerLive, - apiClient, - queryClient, - }) + const yieldOpportunity = yieldDto + ? getYieldOpportunityFromSummary({ + yieldDto, + isLedgerLive, + apiClient, + queryClient, + }) + : getYieldOpportunity({ + yieldId, + isLedgerLive, + apiClient, + queryClient, + }); + + yieldOpportunity .map((yieldDto) => { dispatch({ type: "dashboard/token-yield/select", @@ -526,8 +563,52 @@ export const EarnPageContextProvider = ({ [apiClient, dispatch, isLedgerLive, queryClient] ); + const getDashboardTokenYield = useCallback( + async ({ + category, + tokenBalance, + }: { + category: DashboardYieldCategory; + tokenBalance: TokenBalanceScanResponseDto; + }) => { + const yieldSummaries = await queryClient.fetchQuery({ + queryKey: [ + "dashboard-token-yield-summaries", + tokenString(tokenBalance.token), + tokenBalance.availableYields, + ], + staleTime: 1000 * 60 * 2, + queryFn: ({ signal }) => + fetchYieldSummariesByIds({ + apiClient, + signal, + yieldIds: tokenBalance.availableYields, + }), + }); + + const yieldsById = new Map( + yieldSummaries.map((yieldDto) => [yieldDto.id, yieldDto]) + ); + const yieldId = getDashboardCategoryYieldIdsForToken( + tokenBalance.availableYields, + yieldsById, + category + )[0]; + + return yieldId + ? { + yieldId, + yieldDto: yieldsById.get(yieldId), + } + : null; + }, + [apiClient, queryClient] + ); + + const dashboardTokenSelectionRequestRef = useRef(0); + const onTokenBalanceSelect = useCallback( - (tokenBalance: TokenBalanceScanResponseDto) => { + async (tokenBalance: TokenBalanceScanResponseDto) => { const category = dashboardVariant ? selectedDashboardYieldCategory : null; if (!category) { @@ -535,32 +616,73 @@ export const EarnPageContextProvider = ({ return; } - const yieldId = tokenListYields.yieldIdsByToken.get( - tokenString(tokenBalance.token) - )?.[0]; + const selectionRequestId = dashboardTokenSelectionRequestRef.current + 1; + dashboardTokenSelectionRequestRef.current = selectionRequestId; + dispatch({ type: "token-only/select", data: tokenBalance.token }); + + const tokenYield = await getDashboardTokenYield({ + category, + tokenBalance, + }).catch((error) => { + console.log(error); + return null; + }); - if (!yieldId) return; + if ( + selectionRequestId !== dashboardTokenSelectionRequestRef.current || + !tokenYield + ) { + return; + } - selectDashboardTokenYield({ token: tokenBalance.token, yieldId }); + selectDashboardTokenYield({ + token: tokenBalance.token, + yieldDto: tokenYield.yieldDto, + yieldId: tokenYield.yieldId, + }); }, [ dashboardVariant, dispatch, + getDashboardTokenYield, selectedDashboardYieldCategory, selectDashboardTokenYield, - tokenListYields.yieldIdsByToken, ] ); const onYieldSelect = (yieldId: string) => { - Maybe.fromNullable(multiYields) - .chain((val) => List.find((v) => v.id === yieldId, val)) - .ifJust((val) => dispatch({ type: "yield/select", data: val })); + const yieldSummary = List.find( + (summary) => summary.id === yieldId, + yieldSummaries + ).extractNullable(); + + const yieldOpportunity = yieldSummary + ? getYieldOpportunityFromSummary({ + yieldDto: yieldSummary, + isLedgerLive, + apiClient, + queryClient, + }) + : getYieldOpportunity({ + yieldId, + isLedgerLive, + apiClient, + queryClient, + }); + + yieldOpportunity + .map((yieldDto) => { + dispatch({ type: "yield/select", data: yieldDto }); + return yieldDto; + }) + .run(); }; const onDashboardYieldCategorySelect = (category: DashboardYieldCategory) => { if (selectedDashboardYieldCategory === category) return; + setSelectedDashboardYieldCategoryFallback(category); + const target = dashboardSelectionByCategoryRef.current.get(category) ?? dashboardYieldCatalog.initialSelectionByCategory.get(category); @@ -797,7 +919,7 @@ export const EarnPageContextProvider = ({ tokenBalancesScanLoading || defaultTokensIsLoading; const selectYieldIsLoading = - (selectedStakeId.isNothing() && !hasNotYieldsForToken) || + (autoSelectYield && selectedStakeId.isNothing() && !hasNotYieldsForToken) || initYieldRes.isLoading || yieldOpportunityLoading || tokenBalancesScanLoading || @@ -900,9 +1022,6 @@ export const EarnPageContextProvider = ({ yieldOpportunityLoading, tokenBalancesScanLoading, tokenBalancesData, - tokenMaxYieldRatesByToken: tokenListYields.maxYieldRatesByToken, - tokenYieldCountsByToken: tokenListYields.yieldCountsByToken, - tokenListYieldsIsLoading: tokenListYields.isLoading, onTokenSearch, onValidatorSearch, buttonCTAText, diff --git a/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx b/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx index 2da1ed08..f8642b22 100644 --- a/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx +++ b/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx @@ -49,6 +49,7 @@ export const EarnPageStateUsageBoundaryProvider = ({ const getInitialState = (): State => ({ selectedToken: Maybe.empty(), selectedStakeId: Maybe.empty(), + autoSelectYield: true, selectedValidators: new Map(), stakeAmount: new BigNumber(0), useMaxAmount: false, @@ -89,6 +90,20 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { .orDefault(state); } + case "token-only/select": { + return Maybe.fromFalsy( + state.selectedToken + .map((v) => !equalTokens(v, action.data)) + .orDefault(true) || state.selectedStakeId.isJust() + ) + .map(() => ({ + ...getInitialState(), + selectedToken: Maybe.of(action.data), + autoSelectYield: false, + })) + .orDefault(state); + } + case "dashboard/token-yield/select": { return Maybe.fromFalsy( state.selectedToken @@ -205,6 +220,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { const { selectedToken, selectedStakeId, + autoSelectYield, selectedValidators, stakeAmount: _stakeAmount, useMaxAmount, @@ -321,14 +337,29 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { useEffect(() => { if (shouldWaitForPositionsData) return; + if (!autoSelectYield) return; + selectedStakeId.ifNothing(() => initYield.ifJust(setYield)); - }, [initYield, selectedStakeId, shouldWaitForPositionsData, setYield]); + }, [ + autoSelectYield, + initYield, + selectedStakeId, + shouldWaitForPositionsData, + setYield, + ]); useEffect(() => { if (shouldWaitForPositionsData) return; + if (!autoSelectYield) return; selectedStakeId.ifNothing(() => initYieldRef.current.ifJust(setYield)); - }, [initYieldRef, shouldWaitForPositionsData, selectedStakeId, setYield]); + }, [ + autoSelectYield, + initYieldRef, + shouldWaitForPositionsData, + selectedStakeId, + setYield, + ]); /** * Reset selectedToken if we dont have initToken @@ -365,6 +396,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { const value: State & ExtraData = useMemo( () => ({ selectedStakeId, + autoSelectYield, selectedStake, selectedValidators, stakeAmount, @@ -383,6 +415,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => { }), [ selectedStakeId, + autoSelectYield, selectedStake, selectedToken, selectedValidators, diff --git a/packages/widget/src/pages/details/earn-page/state/types.ts b/packages/widget/src/pages/details/earn-page/state/types.ts index 71598e37..3083b950 100644 --- a/packages/widget/src/pages/details/earn-page/state/types.ts +++ b/packages/widget/src/pages/details/earn-page/state/types.ts @@ -9,7 +9,6 @@ import type { Yield, } from "../../../../domain/types/yields"; import type { ValidatorDto } from "../../../../generated/api/yield"; -import type { TokenMaxYieldRate } from "../../../../hooks/api/use-token-list-yields"; import type { useEstimatedRewards } from "../../../../hooks/use-estimated-rewards"; import type { useProvidersDetails } from "../../../../hooks/use-provider-details"; import type { useRewardTokenDetails } from "../../../../hooks/use-reward-token-details"; @@ -21,6 +20,7 @@ export type State = { selectedStakeId: Maybe< TokenBalanceScanResponseDto["availableYields"][number] >; + autoSelectYield: boolean; selectedValidators: Map; stakeAmount: BigNumber; useMaxAmount: boolean; @@ -29,6 +29,7 @@ export type State = { }; type TokenBalanceSelectAction = Action<"token/select", TokenDto>; +type TokenOnlySelectAction = Action<"token-only/select", TokenDto>; type DashboardTokenYieldSelectAction = Action< "dashboard/token-yield/select", { token: TokenDto; yieldDto: Yield } @@ -52,6 +53,7 @@ type ProviderYieldIdSelectAction = Action< export type Actions = | TokenBalanceSelectAction + | TokenOnlySelectAction | DashboardTokenYieldSelectAction | YieldSelectAction | StakeAmountChangeAction @@ -123,9 +125,6 @@ export type EarnPageContextType = { all: TokenBalanceScanResponseDto[]; filtered: TokenBalanceScanResponseDto[]; }>; - tokenMaxYieldRatesByToken: ReadonlyMap; - tokenYieldCountsByToken: ReadonlyMap; - tokenListYieldsIsLoading: boolean; onTokenSearch: (value: string) => void; onValidatorSearch: (value: string) => void; validatorSearch: string; diff --git a/packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts b/packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts index eb401842..f46a1791 100644 --- a/packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts +++ b/packages/widget/src/pages/details/earn-page/state/use-stake-enter-request-dto.ts @@ -2,11 +2,7 @@ import { List, Maybe } from "purify-ts"; import { useMemo } from "react"; import type { YieldCreateActionDto } from "../../../../domain/types/action"; import type { AddressesDto } from "../../../../domain/types/addresses"; -import { - getYieldActionArg, - isYieldIntegrationAggregator, - type Yield, -} from "../../../../domain/types/yields"; +import { getYieldActionArg, type Yield } from "../../../../domain/types/yields"; import type { ValidatorDto } from "../../../../generated/api/yield"; import { useSKWallet } from "../../../../providers/sk-wallet"; import { useEarnPageState } from "./earn-page-state-context"; @@ -44,25 +40,13 @@ export const useStakeEnterRequestDto = () => { )?.required; const providerId = selectedProviderYieldId.extract(); - if ( - providerIdRequired && - !providerId && - !isYieldIntegrationAggregator(selectedStake) - ) { + if (providerIdRequired && !providerId) { return Maybe.empty(); } - const selectedProviderArgs = isYieldIntegrationAggregator(selectedStake) - ? {} - : { providerId }; + const selectedProviderArgs = providerId ? { providerId } : {}; const validatorsOrProvider = (() => { - if (isYieldIntegrationAggregator(selectedStake)) { - return List.head(validators) - .chainNullable((v) => v.providerId) - .map((providerId) => ({ providerId })); - } - if ( getYieldActionArg(selectedStake, "enter", "validatorAddresses") ?.required diff --git a/packages/widget/src/pages/details/earn-page/types.ts b/packages/widget/src/pages/details/earn-page/types.ts index cff6fd89..2d031d70 100644 --- a/packages/widget/src/pages/details/earn-page/types.ts +++ b/packages/widget/src/pages/details/earn-page/types.ts @@ -1,8 +1,11 @@ -import type { ExtendedYieldType, Yield } from "../../../domain/types/yields"; +import type { + ExtendedYieldType, + YieldBase, +} from "../../../domain/types/yields"; export type SelectedStakeData = { - all: Yield[]; - filtered: Yield[]; + all: YieldBase[]; + filtered: YieldBase[]; groupsWithCounts: Map< ExtendedYieldType, { diff --git a/packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts b/packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts index 3d820b3b..cba512c0 100644 --- a/packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts +++ b/packages/widget/src/pages/position-details/hooks/use-stake-exit-request-dto.ts @@ -2,11 +2,7 @@ import { Just, List, Maybe } from "purify-ts"; import { useMemo } from "react"; import type { YieldCreateActionDto } from "../../../domain/types/action"; import type { AddressesDto } from "../../../domain/types/addresses"; -import { - getYieldActionArg, - isYieldIntegrationAggregator, - type Yield, -} from "../../../domain/types/yields"; +import { getYieldActionArg, type Yield } from "../../../domain/types/yields"; import { useSKWallet } from "../../../providers/sk-wallet"; import { useUnstakeOrPendingActionState } from "../state"; @@ -40,17 +36,7 @@ export const useStakeExitRequestDto = () => { NonNullable, "validatorAddress" | "subnetId" > - | Pick, "providerId"> >(() => { - if (isYieldIntegrationAggregator(val.integrationData)) { - return List.find( - (b) => !!b.validator?.providerId, - val.stakedOrLiquidBalances - ).map((b) => ({ - providerId: b.validator?.providerId, - validatorAddress: b.validator?.address, - })); - } if ( getYieldActionArg( val.integrationData, diff --git a/packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts b/packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts index 28f1346d..e7f06c45 100644 --- a/packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts +++ b/packages/widget/src/pages/position-details/hooks/use-unstake-machine.ts @@ -5,9 +5,6 @@ import { EitherAsync, Maybe } from "purify-ts"; import { type RefObject, useState } from "react"; import { assign, setup } from "xstate"; import { getValidStakeSessionTx } from "../../../domain"; -import type { TransactionVerificationMessageDto } from "../../../domain/types/transaction"; -import type { SKWallet } from "../../../domain/types/wallet"; -import { hasYieldExitSignatureVerification } from "../../../domain/types/yields"; import { useTrackEvent } from "../../../hooks/tracking/use-track-event"; import { useSavedRef } from "../../../hooks/use-saved-ref"; import { useApiClient } from "../../../providers/api/api-client-provider"; @@ -25,17 +22,15 @@ export const useUnstakeMachine = ({ onDone }: { onDone: () => void }) => { ).unsafeCoerce(); const apiClient = useApiClient(); - const { network, address, additionalAddresses, signMessage } = useSKWallet(); + const { address, additionalAddresses } = useSKWallet(); const machineParams = useSavedRef({ onDone, trackEvent, exitStore, apiClient, - signMessage, getData: () => Maybe.fromRecord({ - network: Maybe.fromNullable(network), address: Maybe.fromNullable(address), }).map((val) => ({ ...val, @@ -58,7 +53,6 @@ const getMachine = ( RefObject<{ onDone: () => void; exitStore: ReturnType; - signMessage: ReturnType["signMessage"]; trackEvent: ReturnType; apiClient: ReturnType; getData: () => Maybe< @@ -67,8 +61,7 @@ const getMachine = ( ReturnType >["context"]["data"] > & { - network: NonNullable; - address: NonNullable; + address: string; } >; }> @@ -78,38 +71,21 @@ const getMachine = ( types: { context: {} as { error: Maybe; - transactionVerificationMessageDto: Maybe; - signedMessage: Maybe; data: ReturnType<(typeof ref)["current"]["getData"]>; }, events: {} as | { type: "UNSTAKE" } - | { - type: "__GET_VERIFICATION_MESSAGE__"; - val: GetMaybeJust>; - } | { type: "__SUBMIT__"; val: GetMaybeJust>; } | { type: "__RESET__" } - | { - type: "__GET_VERIFICATION_MESSAGE_SUCCESS__"; - val: TransactionVerificationMessageDto; - } - | { type: "__GET_VERIFICATION_MESSAGE_ERROR__"; val: unknown } - | { type: "CONTINUE_MESSAGE_SIGN" } - | { type: "CANCEL_MESSAGE_SIGN" } - | { type: "__SIGN_MESSAGE_SUCCESS__"; val: string } - | { type: "__SIGN_MESSAGE_ERROR__"; val: unknown } | { type: "__SUBMIT_SUCCESS__" } | { type: "__SUBMIT_ERROR__"; val: unknown }, }, }).createMachine({ context: { error: Maybe.empty(), - transactionVerificationMessageDto: Maybe.empty(), - signedMessage: Maybe.empty(), data: Maybe.empty(), }, on: { UNSTAKE: { target: ".check", reenter: true } }, @@ -119,10 +95,6 @@ const getMachine = ( check: { on: { - __GET_VERIFICATION_MESSAGE__: { - target: "getVerificationMessage", - actions: assign({ data: ({ event }) => Maybe.of(event.val) }), - }, __SUBMIT__: { target: "submit", actions: assign({ data: ({ event }) => Maybe.of(event.val) }), @@ -137,128 +109,12 @@ const getMachine = ( amount: val.requestDto.arguments?.amount, }); - if (hasYieldExitSignatureVerification(val.integrationData)) { - self.send({ type: "__GET_VERIFICATION_MESSAGE__", val }); - } else { - self.send({ type: "__SUBMIT__", val }); - } + self.send({ type: "__SUBMIT__", val }); }, Nothing: () => self.send({ type: "__RESET__" }), }), }, - getVerificationMessage: { - on: { - __GET_VERIFICATION_MESSAGE_SUCCESS__: { - target: "showPopup", - actions: assign(({ context, event }) => ({ - ...context, - transactionVerificationMessageDto: Maybe.of(event.val), - })), - }, - __GET_VERIFICATION_MESSAGE_ERROR__: { - target: ".error", - actions: assign(({ context, event }) => ({ - ...context, - error: Maybe.of(event.val), - })), - }, - }, - initial: "loading", - states: { - loading: { - entry: ({ self, context }) => - EitherAsync.liftEither( - context.data.toEither(new Error("Missing init values")) - ) - .chain((val) => - EitherAsync(() => - ref.current.apiClient.legacy.TransactionControllerGetTransactionVerificationMessageForNetwork( - val.network, - { - payload: { - addresses: { - address: val.addresses.address, - additionalAddresses: - val.addresses.additionalAddresses ?? undefined, - }, - }, - } - ) - ).mapLeft( - () => new Error("Failed to get verification message") - ) - ) - .caseOf({ - Right(v) { - self.send({ - type: "__GET_VERIFICATION_MESSAGE_SUCCESS__", - val: v, - }); - }, - Left(e) { - self.send({ - type: "__GET_VERIFICATION_MESSAGE_ERROR__", - val: e, - }); - }, - }), - }, - - error: {}, - }, - }, - - showPopup: { - on: { - CONTINUE_MESSAGE_SIGN: "signMessage", - CANCEL_MESSAGE_SIGN: "initial", - }, - }, - - signMessage: { - on: { - __SIGN_MESSAGE_SUCCESS__: { - target: "submit", - actions: assign(({ context, event }) => ({ - ...context, - signedMessage: Maybe.of(event.val), - })), - }, - __SIGN_MESSAGE_ERROR__: { - target: ".error", - actions: assign(({ context, event }) => ({ - ...context, - error: Maybe.of(event.val), - })), - }, - }, - initial: "loading", - states: { - loading: { - entry: ({ self, context }) => - EitherAsync.liftEither( - context.transactionVerificationMessageDto.toEither( - new Error("Missing transaction verification message") - ) - ) - .chain((val) => ref.current.signMessage(val.message)) - .caseOf({ - Right(v) { - self.send({ - type: "__SIGN_MESSAGE_SUCCESS__", - val: v, - }); - }, - Left(l) { - self.send({ type: "__SIGN_MESSAGE_ERROR__", val: l }); - }, - }), - }, - error: {}, - }, - }, - submit: { on: { __SUBMIT_SUCCESS__: "done", @@ -273,48 +129,8 @@ const getMachine = ( initial: "loading", states: { loading: { - entry: ({ - self, - context: { - data, - signedMessage, - transactionVerificationMessageDto, - }, - }) => - EitherAsync.liftEither( - data - .map((val) => - Maybe.fromRecord({ - data: Maybe.of(val), - requestDto: Maybe.of(val.requestDto), - transactionVerificationMessageDto, - signedMessage, - }) - .map( - (val) => - ({ - ...val.data, - requestDto: { - ...val.requestDto, - address: val.data.addresses.address, - arguments: { - ...(val.requestDto.arguments ?? {}), - // The backend still accepts this legacy verification bag - // even though the checked-in schema does not expose it yet. - signatureVerification: { - message: - val.transactionVerificationMessageDto - .message, - signed: val.signedMessage, - }, - } as typeof val.requestDto.arguments, - }, - }) as typeof val.data - ) - .orDefault(val) - ) - .toEither(new Error("Missing params")) - ) + entry: ({ self, context: { data } }) => + EitherAsync.liftEither(data.toEither(new Error("Missing params"))) .chain((val) => EitherAsync(() => ref.current.apiClient.yield.ActionsControllerExitYield({ diff --git a/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts b/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts index 8ba6ace8..e85dfead 100644 --- a/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts +++ b/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts @@ -7,7 +7,6 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; import { getTransactionGasEstimate } from "../../../domain/types/action"; import { getKycProviderName } from "../../../domain/types/kyc"; -import { getYieldCommission } from "../../../domain/types/yields"; import { useTokensPrices } from "../../../hooks/api/use-tokens-prices"; import { useYieldKycGate } from "../../../hooks/api/use-yield-kyc-gate"; import { usePositionDetailsStakeMatch } from "../../../hooks/navigation/use-position-details-stake-match"; @@ -19,7 +18,7 @@ import { useYieldType } from "../../../hooks/use-yield-type"; import { useApiClient } from "../../../providers/api/api-client-provider"; import { useEnterStakeStore } from "../../../providers/enter-stake-store"; import { useSettings } from "../../../providers/settings"; -import { APToPercentage, defaultFormattedNumber } from "../../../utils"; +import { defaultFormattedNumber } from "../../../utils"; import { getGasFeeInUSD } from "../../../utils/formatters"; import { useRegisterFooterButton } from "../../components/footer-outlet/context"; import type { MetaInfoProps } from "../pages/common-page/common.page"; @@ -231,17 +230,6 @@ export const useStakeReview = () => { [selectedStake, selectedToken, enterRequest.selectedValidators, variant] ); - const commissionFee = useMemo( - () => - selectedStake - .chainNullable(getYieldCommission) - .map((commission) => - commission.reduce((acc, curr) => acc + curr.value, 0) - ) - .map((val) => `${APToPercentage(val)}%`), - [selectedStake] - ); - return { token: selectedToken, amount, @@ -260,7 +248,7 @@ export const useStakeReview = () => { managementFee, performanceFee, feeConfigLoading: actionPreviewQuery.isLoading, - commissionFee, + commissionFee: Maybe.empty(), kycGate: yieldKycGate.gate, kycProviderName, kycStatusIsChecking: diff --git a/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts b/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts index c1db7a5c..d9ebb78e 100644 --- a/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts +++ b/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts @@ -146,16 +146,8 @@ export const useUnstakeActionReview = () => { const unstakeIsLoading = machineState.matches("check") || - machineState.matches({ getVerificationMessage: "loading" }) || - machineState.matches({ signMessage: "loading" }) || machineState.matches({ submit: "loading" }); - const showUnstakeSignMessagePopup = machineState.matches("showPopup"); - - const onContinueUnstakeSignMessage = () => - send({ type: "CONTINUE_MESSAGE_SIGN" }); - const onCloseUnstakeSignMessage = () => send({ type: "CANCEL_MESSAGE_SIGN" }); - const onClick = () => { if (unstakeIsLoading || kycGateIsBlocking) return; @@ -190,9 +182,9 @@ export const useUnstakeActionReview = () => { rewardTokenDetailsProps, token: interactedToken, metaInfo, - onContinueUnstakeSignMessage, - onCloseUnstakeSignMessage, - showUnstakeSignMessagePopup, + onContinueUnstakeSignMessage: () => {}, + onCloseUnstakeSignMessage: () => {}, + showUnstakeSignMessagePopup: false, gasCheckLoading: actionPreviewQuery.isLoading || actionPreviewQuery.isFetching || diff --git a/packages/widget/src/providers/api/api-client.ts b/packages/widget/src/providers/api/api-client.ts index a109d3ec..5e1cd74f 100644 --- a/packages/widget/src/providers/api/api-client.ts +++ b/packages/widget/src/providers/api/api-client.ts @@ -119,17 +119,12 @@ const bindLegacyApi = ({ api.TokenControllerTokenBalancesScan, options ), - TransactionControllerGetTransactionVerificationMessageForNetwork: - bindOperation( - api.TransactionControllerGetTransactionVerificationMessageForNetwork, - options - ), - YieldControllerGetSingleYieldRewardsSummary: bindOperation( - api.YieldControllerGetSingleYieldRewardsSummary, + YieldControllerGetMyNetworks: bindOperation( + api.YieldControllerGetMyNetworks, options ), - YieldControllerYieldOpportunity: bindOperation( - api.YieldControllerYieldOpportunity, + YieldControllerGetSingleYieldRewardsSummary: bindOperation( + api.YieldControllerGetSingleYieldRewardsSummary, options ), }); @@ -159,10 +154,6 @@ const bindYieldApi = ({ ), HealthControllerHealth: bindOperation(api.HealthControllerHealth, options), KycControllerGetStatus: bindOperation(api.KycControllerGetStatus, options), - NetworksControllerGetNetworks: bindOperation( - api.NetworksControllerGetNetworks, - options - ), ProvidersControllerGetProvider: bindOperation( api.ProvidersControllerGetProvider, options diff --git a/packages/widget/src/providers/ethereum/config.ts b/packages/widget/src/providers/ethereum/config.ts index 77e040cb..ae0d5288 100644 --- a/packages/widget/src/providers/ethereum/config.ts +++ b/packages/widget/src/providers/ethereum/config.ts @@ -23,17 +23,19 @@ const queryFn = async ({ apiClient, queryClient, forceWalletConnectOnly, + institutionalWallets, variant, }: { apiClient: ApiClient; queryClient: QueryClient; forceWalletConnectOnly: boolean; + institutionalWallets: boolean; variant: VariantProps["variant"]; }): Promise<{ evmChainsMap: Partial; evmChains: Chain[]; connector: Maybe; - fineryWallets: ReturnType | null; + institutionalWallets: ReturnType | null; }> => getEnabledNetworks({ apiClient, queryClient }).caseOf({ Right: (networks) => { @@ -80,8 +82,10 @@ const queryFn = async ({ evmChainsMap: filteredEvmChainsMap, evmChains, connector: Maybe.fromPredicate(() => !!evmChains.length, connector), - fineryWallets: - variant === "finery" ? createFineryWallets(evmChains) : null, + institutionalWallets: + variant === "finery" || institutionalWallets + ? createFineryWallets(evmChains) + : null, }); }, Left: (l) => Promise.reject(l), @@ -91,7 +95,13 @@ export const getConfig = (opts: Parameters[0]) => EitherAsync(() => opts.queryClient.fetchQuery({ staleTime: Number.POSITIVE_INFINITY, - queryKey: [config.appPrefix, "evm-config"], + queryKey: [ + config.appPrefix, + "evm-config", + opts.variant, + opts.institutionalWallets, + opts.forceWalletConnectOnly, + ], queryFn: () => queryFn(opts), }) ).mapLeft((e) => { diff --git a/packages/widget/src/providers/settings/types.ts b/packages/widget/src/providers/settings/types.ts index c1aae9cc..10d1a0f9 100644 --- a/packages/widget/src/providers/settings/types.ts +++ b/packages/widget/src/providers/settings/types.ts @@ -84,6 +84,7 @@ export type SettingsProps = { | Record | ((chain: SupportedSKChains) => string); dashboardVariant?: boolean; + institutionalWallets?: boolean; hideChainSelector?: boolean; hideAccountAndChainSelector?: boolean; preferredTokenYieldsPerNetwork?: PreferredTokenYieldsPerNetwork; diff --git a/packages/widget/src/providers/wagmi/index.ts b/packages/widget/src/providers/wagmi/index.ts index 524920dd..cea4c7a9 100644 --- a/packages/widget/src/providers/wagmi/index.ts +++ b/packages/widget/src/providers/wagmi/index.ts @@ -60,6 +60,9 @@ const omitEnsUniversalResolver = (chain: T): T => { return { ...chain, contracts } as T; }; +const withoutEmptyWalletGroups = (walletList: WalletList): WalletList => + walletList.filter((walletGroup) => walletGroup.wallets.length > 0); + export type BuildWagmiConfig = typeof buildWagmiConfig; const buildWagmiConfig = async (opts: { @@ -82,6 +85,7 @@ const buildWagmiConfig = async (opts: { isLedgerLive: boolean; isSafe: boolean; chainIconMapping: SettingsProps["chainIconMapping"]; + institutionalWallets: boolean; variant: VariantProps["variant"]; solanaWallets: SolanaWallet[]; solanaConnection: Connection; @@ -104,6 +108,7 @@ const buildWagmiConfig = async (opts: { Promise.all([ getEvmConfig({ forceWalletConnectOnly: opts.forceWalletConnectOnly, + institutionalWallets: opts.institutionalWallets, queryClient: opts.queryClient, variant: opts.variant, apiClient: opts.apiClient, @@ -230,15 +235,15 @@ const buildWagmiConfig = async (opts: { const walletList = Just(null) .map(() => { - if (evmConfig.fineryWallets) { + if (evmConfig.institutionalWallets) { return [ { groupName: "Primary", - wallets: evmConfig.fineryWallets.primaryWallets, + wallets: evmConfig.institutionalWallets.primaryWallets, }, { groupName: "Other", - wallets: evmConfig.fineryWallets.otherWallets, + wallets: evmConfig.institutionalWallets.otherWallets, }, ...Maybe.catMaybes(miscConfig.connectors), ]; @@ -292,6 +297,7 @@ const buildWagmiConfig = async (opts: { })) ) .map((walletList) => opts.mapWalletListFn?.(walletList) ?? walletList) + .map(withoutEmptyWalletGroups) .map((walletList) => { return walletList.map((wg) => ({ ...wg, @@ -385,7 +391,6 @@ const buildWagmiConfig = async (opts: { }); }; -const queryKey = [config.appPrefix, "wagmi-config"]; const staleTime = Number.POSITIVE_INFINITY; export const useWagmiConfig = () => { @@ -396,6 +401,7 @@ export const useWagmiConfig = () => { disableInjectedProviderDiscovery, mapWalletFn, chainIconMapping, + institutionalWallets, variant, mapWalletListFn, tonConnectManifestUrl, @@ -412,7 +418,13 @@ export const useWagmiConfig = () => { const wagmiConfigQuery = useQuery({ staleTime, - queryKey, + queryKey: [ + config.appPrefix, + "wagmi-config", + variant, + !!institutionalWallets, + !!wagmi?.forceWalletConnectOnly, + ], queryFn: () => buildWagmiConfig({ mapWalletFn, @@ -427,6 +439,7 @@ export const useWagmiConfig = () => { externalProviders: externalProvidersRef, }), chainIconMapping, + institutionalWallets: !!institutionalWallets, variant, solanaWallets: solanaWallets.wallets, solanaConnection: solanaConnection.connection, diff --git a/packages/widget/tests/components/select-validator-section.test.tsx b/packages/widget/tests/components/select-validator-section.test.tsx index 4d340d98..94a75a43 100644 --- a/packages/widget/tests/components/select-validator-section.test.tsx +++ b/packages/widget/tests/components/select-validator-section.test.tsx @@ -8,11 +8,7 @@ import type { useSelectValidator } from "../../src/pages/details/earn-page/compo import { UtilaSelectValidatorSection } from "../../src/pages-dashboard/overview/earn-page/utila-select-validator-section"; import { SettingsContextProvider } from "../../src/providers/settings"; import { i18nInstance } from "../../src/translation"; -import { - legacyYieldFixture, - yieldApiValidatorFixture, - yieldApiYieldFixture, -} from "../fixtures"; +import { yieldApiValidatorFixture, yieldApiYieldFixture } from "../fixtures"; import { describe, expect, it } from "../utils/test-extend"; import { render } from "../utils/test-utils"; @@ -47,7 +43,6 @@ const selectedStake = { exit: { fields: [] }, }, }, - __fallback__: legacyYieldFixture(), } as Yield; const createHookValue = ( diff --git a/packages/widget/tests/domain/kyc.test.ts b/packages/widget/tests/domain/kyc.test.ts index 62d00134..01cfe1c5 100644 --- a/packages/widget/tests/domain/kyc.test.ts +++ b/packages/widget/tests/domain/kyc.test.ts @@ -5,16 +5,11 @@ import { mapKycStatusToGate, } from "../../src/domain/types/kyc"; import type { Yield } from "../../src/domain/types/yields"; -import { - legacyYieldFixture, - yieldApiProviderFixture, - yieldApiYieldFixture, -} from "../fixtures"; +import { yieldApiProviderFixture, yieldApiYieldFixture } from "../fixtures"; const createYield = (overrides?: Partial): Yield => ({ ...yieldApiYieldFixture(), - __fallback__: legacyYieldFixture(), provider: yieldApiProviderFixture({ name: "Superstate", website: "https://superstate.com", diff --git a/packages/widget/tests/fixtures/index.ts b/packages/widget/tests/fixtures/index.ts index 6b1dea07..5eac6de2 100644 --- a/packages/widget/tests/fixtures/index.ts +++ b/packages/widget/tests/fixtures/index.ts @@ -7,15 +7,16 @@ import { EvmNetworks } from "../../src/domain/types/chains/networks"; import type { YieldBalanceDto } from "../../src/domain/types/positions"; import type { YieldRewardRateDto } from "../../src/domain/types/reward-rate"; import type { Yield } from "../../src/domain/types/yields"; -import type { TokenDto as LegacyTokenDto } from "../../src/generated/api/legacy"; +import type { + TokenDto as LegacyTokenDto, + YieldDto as LegacyYieldDto, +} from "../../src/generated/api/legacy"; import type { ValidatorDto, - NetworkDto as YieldApiNetworkDto, ProviderDto as YieldApiProviderDto, } from "../../src/generated/api/yield"; -type YieldApiYieldDto = Omit; -type LegacyYieldDto = Yield["__fallback__"]; +type YieldApiYieldDto = Omit; const apyFaker = () => faker.number.float({ min: 0, max: 0.05 }); @@ -40,91 +41,6 @@ const yieldApiTokenFixture = ( ...overrides, }); -const miscNetworks = new Set([ - "aptos", - "cardano", - "near", - "solana", - "solana-devnet", - "stellar", - "stellar-testnet", - "sui", - "tezos", - "tron", - "ton", - "ton-testnet", - "hyperliquid", -]); - -const substrateNetworks = new Set([ - "polkadot", - "kusama", - "westend", - "bittensor", -]); - -const getYieldApiNetworkCategory = ( - id: YieldApiNetworkDto["id"] -): YieldApiNetworkDto["category"] => { - if (miscNetworks.has(id)) return "misc"; - if (substrateNetworks.has(id)) return "substrate"; - if ( - [ - "ethereum", - "ethereum-goerli", - "ethereum-holesky", - "ethereum-sepolia", - "ethereum-hoodi", - "arbitrum", - "base", - "base-sepolia", - "gnosis", - "optimism", - "polygon", - "polygon-amoy", - "starknet", - "zksync", - "linea", - "unichain", - "monad-testnet", - "monad", - "avalanche-c", - "avalanche-c-atomic", - "avalanche-p", - "binance", - "celo", - "fantom", - "harmony", - "moonriver", - "okc", - "viction", - "core", - "sonic", - "plasma", - "katana", - "hyperevm", - ].includes(id) - ) { - return "evm"; - } - - return "cosmos"; -}; - -export const yieldApiNetworkFixture = ( - overrides?: Partial -): YieldApiNetworkDto => { - const id = overrides?.id ?? "ethereum"; - - return { - id, - name: id, - category: getYieldApiNetworkCategory(id), - logoURI: `https://assets.stakek.it/networks/${id}.svg`, - ...overrides, - }; -}; - export const yieldApiProviderFixture = ( overrides?: Partial ): YieldApiProviderDto => ({ diff --git a/packages/widget/tests/mocks/legacy-api-handlers.ts b/packages/widget/tests/mocks/legacy-api-handlers.ts index 16d9a0c9..af754e88 100644 --- a/packages/widget/tests/mocks/legacy-api-handlers.ts +++ b/packages/widget/tests/mocks/legacy-api-handlers.ts @@ -23,6 +23,12 @@ const defaultYield = legacyYieldFixture({ }); export const getLegacyApiMock = () => [ + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { + await delay(); + + return HttpResponse.json([defaultToken.network]); + }), + http.get(legacyApiRoute("/v1/tokens"), async () => { await delay(); diff --git a/packages/widget/tests/mocks/yield-api-handlers.ts b/packages/widget/tests/mocks/yield-api-handlers.ts index 55284cc8..11acdd49 100644 --- a/packages/widget/tests/mocks/yield-api-handlers.ts +++ b/packages/widget/tests/mocks/yield-api-handlers.ts @@ -6,7 +6,6 @@ import type { import type { TokenDto } from "../../src/domain/types/tokens"; import { yieldApiActionFixture, - yieldApiNetworkFixture, yieldApiProviderFixture, yieldApiTransactionFixture, yieldApiValidatorsFixture, @@ -67,14 +66,6 @@ export const getYieldApiMock = () => [ }); }), - http.get(yieldApiRoute("/v1/networks"), async () => { - await delay(); - - return HttpResponse.json([ - yieldApiNetworkFixture({ id: defaultToken.network }), - ]); - }), - http.get(yieldApiRoute("/v1/yields"), async () => { await delay(); diff --git a/packages/widget/tests/pages-dashboard/position-details-model.test.tsx b/packages/widget/tests/pages-dashboard/position-details-model.test.tsx index 7250838b..04224407 100644 --- a/packages/widget/tests/pages-dashboard/position-details-model.test.tsx +++ b/packages/widget/tests/pages-dashboard/position-details-model.test.tsx @@ -8,7 +8,6 @@ import { getDashboardPositionDetailsModel, } from "../../src/pages-dashboard/position-details/position-details-model"; import { - legacyYieldFixture, yieldApiProviderFixture, yieldApiYieldFixture, yieldBalanceFixture, @@ -71,7 +70,6 @@ const makeYield = (overrides?: Partial): Yield => symbol: "rETH", }, }), - __fallback__: legacyYieldFixture(), provider: yieldApiProviderFixture({ name: "Rocket Pool" }), ...overrides, }) as Yield; diff --git a/packages/widget/tests/use-cases/deep-links-flow/setup.ts b/packages/widget/tests/use-cases/deep-links-flow/setup.ts index 098c375a..ae468094 100644 --- a/packages/widget/tests/use-cases/deep-links-flow/setup.ts +++ b/packages/widget/tests/use-cases/deep-links-flow/setup.ts @@ -6,7 +6,6 @@ import { waitForMs } from "../../../src/utils"; import { legacyYieldFixture, yieldApiActionFixture, - yieldApiNetworkFixture, yieldApiTransactionFixture, yieldApiValidatorFixture, yieldApiValidatorsFixture, @@ -266,12 +265,9 @@ export const setup = async ( { token: ether, availableYields: ["ethereum-eth-etherfi-staking"] }, ]); }), - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); - return HttpResponse.json([ - yieldApiNetworkFixture({ id: token.network }), - yieldApiNetworkFixture({ id: ether.network }), - ]); + return HttpResponse.json([token.network, ether.network]); }), http.post(legacyApiRoute("/v1/tokens/balances/scan"), async () => { diff --git a/packages/widget/tests/use-cases/external-provider/setup.ts b/packages/widget/tests/use-cases/external-provider/setup.ts index 8df6c9ef..a808ca50 100644 --- a/packages/widget/tests/use-cases/external-provider/setup.ts +++ b/packages/widget/tests/use-cases/external-provider/setup.ts @@ -1,7 +1,6 @@ import { delay, HttpResponse, http } from "msw"; import { legacyYieldFixture, - yieldApiNetworkFixture, yieldApiValidatorsFixture, yieldApiYieldFixture, } from "../../fixtures"; @@ -123,15 +122,13 @@ export const setup = (worker: TestWorker) => { }); worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); return HttpResponse.json([ - yieldApiNetworkFixture({ id: etherNativeStaking.token.network }), - yieldApiNetworkFixture({ - id: avalancheAvaxNativeStaking.token.network, - }), - yieldApiNetworkFixture({ id: solanaNativeStaking.token.network }), - yieldApiNetworkFixture({ id: tonNativeStaking.token.network }), + etherNativeStaking.token.network, + avalancheAvaxNativeStaking.token.network, + solanaNativeStaking.token.network, + tonNativeStaking.token.network, ]); }), diff --git a/packages/widget/tests/use-cases/gas-warning-flow/setup.ts b/packages/widget/tests/use-cases/gas-warning-flow/setup.ts index fb427f68..d89be9f4 100644 --- a/packages/widget/tests/use-cases/gas-warning-flow/setup.ts +++ b/packages/widget/tests/use-cases/gas-warning-flow/setup.ts @@ -5,7 +5,6 @@ import { waitForMs } from "../../../src/utils"; import { legacyYieldFixture, yieldApiActionFixture, - yieldApiNetworkFixture, yieldApiTransactionFixture, yieldApiValidatorsFixture, yieldApiYieldFixture, @@ -151,11 +150,9 @@ export const setup = (worker: TestWorker) => { }) => yieldsTxGasAmountMap.set(yieldId, amount); worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); - return HttpResponse.json([ - yieldApiNetworkFixture({ id: avalancheCToken.network }), - ]); + return HttpResponse.json([avalancheCToken.network]); }), http.get(legacyApiRoute("/v1/tokens"), async () => { diff --git a/packages/widget/tests/use-cases/geo-block.test.tsx b/packages/widget/tests/use-cases/geo-block.test.tsx index 2f3baa37..3f9e308d 100644 --- a/packages/widget/tests/use-cases/geo-block.test.tsx +++ b/packages/widget/tests/use-cases/geo-block.test.tsx @@ -1,12 +1,12 @@ import { HttpResponse, http } from "msw"; -import { yieldApiRoute } from "../mocks/api-routes"; +import { legacyApiRoute } from "../mocks/api-routes"; import { describe, expect, it } from "../utils/test-extend"; import { renderApp } from "../utils/test-utils"; describe("Geo block", () => { it("Show geo block popup", async ({ worker }) => { worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { return HttpResponse.json( { code: 403, diff --git a/packages/widget/tests/use-cases/renders-initial-page.test.tsx b/packages/widget/tests/use-cases/renders-initial-page.test.tsx index 1b464802..bc09f210 100644 --- a/packages/widget/tests/use-cases/renders-initial-page.test.tsx +++ b/packages/widget/tests/use-cases/renders-initial-page.test.tsx @@ -1,9 +1,5 @@ import { delay, HttpResponse, http } from "msw"; -import { - legacyYieldFixture, - yieldApiNetworkFixture, - yieldApiYieldFixture, -} from "../fixtures"; +import { legacyYieldFixture, yieldApiYieldFixture } from "../fixtures"; import { legacyApiRoute, yieldApiRoute } from "../mocks/api-routes"; import { describe, expect, it } from "../utils/test-extend"; import { renderApp } from "../utils/test-utils"; @@ -81,13 +77,11 @@ describe("Renders initial page", () => { }); worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); return HttpResponse.json([ - yieldApiNetworkFixture({ id: etherNativeStaking.token.network }), - yieldApiNetworkFixture({ - id: avalancheAvaxNativeStaking.token.network, - }), + etherNativeStaking.token.network, + avalancheAvaxNativeStaking.token.network, ]); }), diff --git a/packages/widget/tests/use-cases/select-opportunity.test.tsx b/packages/widget/tests/use-cases/select-opportunity.test.tsx index 4dd855dd..f7983946 100644 --- a/packages/widget/tests/use-cases/select-opportunity.test.tsx +++ b/packages/widget/tests/use-cases/select-opportunity.test.tsx @@ -2,7 +2,6 @@ import { delay, HttpResponse, http } from "msw"; import { userEvent } from "vitest/browser"; import { legacyYieldFixture, - yieldApiNetworkFixture, yieldApiYieldFixture, yieldRiskSummaryFixture, } from "../fixtures"; @@ -139,61 +138,57 @@ describe("Select opportunity", () => { }; worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); - return HttpResponse.json( - ( - [ - "ethereum", - "ethereum-goerli", - "avalanche-c", - "celo", - "akash", - "cosmos", - "kava", - "osmosis", - "juno", - "stargaze", - "persistence", - "axelar", - "onomy", - "quicksilver", - "agoric", - "band-protocol", - "bitsong", - "chihuahua", - "comdex", - "crescent", - "cronos", - "cudos", - "fetch-ai", - "gravity-bridge", - "injective", - "irisnet", - "ki-network", - "mars-protocol", - "regen", - "secret", - "sentinel", - "sommelier", - "teritori", - "umee", - "coreum", - "desmos", - "dydx", - "optimism", - "fantom", - "arbitrum", - "polygon", - "binance", - "near", - "harmony", - "solana", - "tezos", - ] as const - ).map((id) => yieldApiNetworkFixture({ id })) - ); + return HttpResponse.json([ + "ethereum", + "ethereum-goerli", + "avalanche-c", + "celo", + "akash", + "cosmos", + "kava", + "osmosis", + "juno", + "stargaze", + "persistence", + "axelar", + "onomy", + "quicksilver", + "agoric", + "band-protocol", + "bitsong", + "chihuahua", + "comdex", + "crescent", + "cronos", + "cudos", + "fetch-ai", + "gravity-bridge", + "injective", + "irisnet", + "ki-network", + "mars-protocol", + "regen", + "secret", + "sentinel", + "sommelier", + "teritori", + "umee", + "coreum", + "desmos", + "dydx", + "optimism", + "fantom", + "arbitrum", + "polygon", + "binance", + "near", + "harmony", + "solana", + "tezos", + ]); }), http.get(legacyApiRoute("/v1/tokens"), async () => { await delay(); diff --git a/packages/widget/tests/use-cases/sk-wallet.test.tsx b/packages/widget/tests/use-cases/sk-wallet.test.tsx index f353a3ee..f44174b5 100644 --- a/packages/widget/tests/use-cases/sk-wallet.test.tsx +++ b/packages/widget/tests/use-cases/sk-wallet.test.tsx @@ -9,8 +9,7 @@ import { SKWalletProvider, useSKWallet } from "../../src/providers/sk-wallet"; import { SolanaProvider } from "../../src/providers/solana"; import { TrackingContextProviderWithProps } from "../../src/providers/tracking"; import { WagmiConfigProvider } from "../../src/providers/wagmi/provider"; -import { yieldApiNetworkFixture } from "../fixtures"; -import { yieldApiRoute } from "../mocks/api-routes"; +import { legacyApiRoute } from "../mocks/api-routes"; import { describe, expect, it, vi } from "../utils/test-extend"; import { renderHook } from "../utils/test-utils"; @@ -45,11 +44,9 @@ describe("SK Wallet", () => { const sendTransactionSpy = vi.fn(async () => "hash"); worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); - return HttpResponse.json([ - yieldApiNetworkFixture({ id: MiscNetworks.Solana }), - ]); + return HttpResponse.json([MiscNetworks.Solana]); }) ); @@ -124,11 +121,9 @@ describe("SK Wallet", () => { const sendTransactionSpy = vi.fn(async (_: unknown) => "hash"); worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); - return HttpResponse.json([ - yieldApiNetworkFixture({ id: MiscNetworks.Ton }), - ]); + return HttpResponse.json([MiscNetworks.Ton]); }) ); diff --git a/packages/widget/tests/use-cases/staking-flow/setup.ts b/packages/widget/tests/use-cases/staking-flow/setup.ts index 221e73aa..970eaddf 100644 --- a/packages/widget/tests/use-cases/staking-flow/setup.ts +++ b/packages/widget/tests/use-cases/staking-flow/setup.ts @@ -12,7 +12,6 @@ import type { import { waitForMs } from "../../../src/utils"; import { yieldApiActionFixture, - yieldApiNetworkFixture, yieldApiProviderFixture, yieldApiTransactionFixture, yieldApiValidatorsFixture, @@ -238,9 +237,9 @@ export const setup = async (worker: TestWorker) => { } as unknown as TransactionDto; worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); - return HttpResponse.json([yieldApiNetworkFixture({ id: "avalanche-c" })]); + return HttpResponse.json(["avalanche-c"]); }), http.get(legacyApiRoute("/v1/tokens"), async () => { @@ -433,7 +432,6 @@ export const setup = async (worker: TestWorker) => { name: "Benqi", website: "https://benqi.fi/", }), - __fallback__: yieldOp, }; return { diff --git a/packages/widget/tests/use-cases/trust-incentive-apy/setup.ts b/packages/widget/tests/use-cases/trust-incentive-apy/setup.ts index 80f6b1ab..0cb77642 100644 --- a/packages/widget/tests/use-cases/trust-incentive-apy/setup.ts +++ b/packages/widget/tests/use-cases/trust-incentive-apy/setup.ts @@ -7,7 +7,6 @@ import type { Yield } from "../../../src/domain/types/yields"; import { waitForMs } from "../../../src/utils"; import { legacyYieldFixture, - yieldApiNetworkFixture, yieldApiYieldFixture, yieldBalanceFixture, yieldRewardRateFixture, @@ -17,7 +16,7 @@ import { rkMockWallet } from "../../utils/mock-connector"; import type { TestWorker } from "../../utils/test-extend"; type LegacyTokenDto = ReturnType["token"]; -type YieldApiYieldDto = Omit; +type YieldApiYieldDto = Omit; const setUrl = ({ accountId, @@ -274,9 +273,9 @@ export const setup = async ( }); worker.use( - http.get(yieldApiRoute("/v1/networks"), async () => { + http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => { await delay(); - return HttpResponse.json([yieldApiNetworkFixture({ id: token.network })]); + return HttpResponse.json([token.network]); }), http.get(legacyApiRoute("/v1/tokens"), async () => { await delay();