diff --git a/src/lib/yieldxyz/utils.test.ts b/src/lib/yieldxyz/utils.test.ts index a43d87a5262..2b508d154ea 100644 --- a/src/lib/yieldxyz/utils.test.ts +++ b/src/lib/yieldxyz/utils.test.ts @@ -11,6 +11,7 @@ import { getYieldActionLabelKeys, getYieldSuccessMessageKey, isStakingYieldType, + resolveAssetSymbolForTx, resolveYieldInputAssetIcon, searchValidators, searchYields, @@ -49,6 +50,40 @@ describe('getTransactionButtonText', () => { }) }) +describe('resolveAssetSymbolForTx', () => { + it('should return output token for exit APPROVAL', () => { + expect(resolveAssetSymbolForTx('APPROVAL', 'exit', 'AVAX', 'sAVAX')).toBe('sAVAX') + expect(resolveAssetSymbolForTx('APPROVE', 'exit', 'AVAX', 'sAVAX')).toBe('sAVAX') + expect(resolveAssetSymbolForTx('APPROVAL', 'exit', 'ETH', 'rETH')).toBe('rETH') + expect(resolveAssetSymbolForTx('APPROVAL', 'exit', 'ETH', 'stETH')).toBe('stETH') + }) + + it('should return input token for non-APPROVAL exit types', () => { + expect(resolveAssetSymbolForTx('UNSTAKE', 'exit', 'AVAX', 'sAVAX')).toBe('AVAX') + expect(resolveAssetSymbolForTx('WITHDRAW', 'exit', 'USDT', 'cUSDTv3')).toBe('USDT') + expect(resolveAssetSymbolForTx('SWAP', 'exit', 'ETH', 'rETH')).toBe('ETH') + expect(resolveAssetSymbolForTx('EXIT', 'exit', 'ETH', 'stETH')).toBe('ETH') + }) + + it('should return input token for enter actions', () => { + expect(resolveAssetSymbolForTx('APPROVAL', 'enter', 'USDT', 'cUSDTv3')).toBe('USDT') + expect(resolveAssetSymbolForTx('STAKE', 'enter', 'ETH', 'stETH')).toBe('ETH') + expect(resolveAssetSymbolForTx('DEPOSIT', 'enter', 'USDT', 'cUSDTv3')).toBe('USDT') + }) + + it('should return input token for manage actions', () => { + expect(resolveAssetSymbolForTx('CLAIM', 'manage', 'ETH', 'stETH')).toBe('ETH') + }) + + it('should fallback to input token when output token is undefined', () => { + expect(resolveAssetSymbolForTx('APPROVAL', 'exit', 'ETH', undefined)).toBe('ETH') + }) + + it('should fallback to input token when tx type is undefined', () => { + expect(resolveAssetSymbolForTx(undefined, 'exit', 'ETH', 'stETH')).toBe('ETH') + }) +}) + describe('resolveYieldInputAssetIcon', () => { it('should prefer inputToken assetId', () => { const yieldItem = { diff --git a/src/lib/yieldxyz/utils.ts b/src/lib/yieldxyz/utils.ts index e2c6b0c1af6..b320f814523 100644 --- a/src/lib/yieldxyz/utils.ts +++ b/src/lib/yieldxyz/utils.ts @@ -67,6 +67,10 @@ const TX_TYPE_TO_LABELS: Record = { type TerminologyKey = 'staking' | 'vault' +const assertNever = (value: never): never => { + throw new Error(`Unhandled yield type: ${value}`) +} + export const isStakingYieldType = (yieldType: YieldType): boolean => { switch (yieldType) { case 'staking': @@ -79,8 +83,7 @@ export const isStakingYieldType = (yieldType: YieldType): boolean => { case 'lending': return false default: - assertNever(yieldType) - return false + return assertNever(yieldType) } } @@ -110,6 +113,19 @@ export const getTransactionButtonText = ( return 'Confirm' } +const APPROVAL_TX_TYPES = new Set(['APPROVAL', 'APPROVE']) + +export const resolveAssetSymbolForTx = ( + txType: string | undefined, + action: 'enter' | 'exit' | 'manage', + assetSymbol: string, + outputTokenSymbol: string | undefined, +): string => { + if (action !== 'exit' || !outputTokenSymbol || !txType) return assetSymbol + if (APPROVAL_TX_TYPES.has(txType.toUpperCase())) return outputTokenSymbol + return assetSymbol +} + export const formatYieldTxTitle = ( title: string, assetSymbol: string, @@ -217,10 +233,6 @@ export type YieldActionLabelKeys = { exit: string } -const assertNever = (value: never): never => { - throw new Error(`Unhandled yield type: ${value}`) -} - /** * Gets the appropriate translation keys for yield actions based on yield type. * diff --git a/src/pages/Yields/hooks/useYieldTransactionFlow.ts b/src/pages/Yields/hooks/useYieldTransactionFlow.ts index b523aca1d06..2935b19d827 100644 --- a/src/pages/Yields/hooks/useYieldTransactionFlow.ts +++ b/src/pages/Yields/hooks/useYieldTransactionFlow.ts @@ -1,6 +1,13 @@ import { useToast } from '@chakra-ui/react' import type { AssetId, ChainId } from '@shapeshiftoss/caip' -import { cosmosChainId, ethChainId, fromAccountId, usdtAssetId } from '@shapeshiftoss/caip' +import { + CHAIN_NAMESPACE, + cosmosChainId, + ethChainId, + fromAccountId, + fromChainId, + usdtAssetId, +} from '@shapeshiftoss/caip' import { assertGetViemClient } from '@shapeshiftoss/contracts' import type { KnownChainIds } from '@shapeshiftoss/types' import { useQuery, useQueryClient } from '@tanstack/react-query' @@ -20,7 +27,11 @@ import type { CosmosStakeArgs } from '@/lib/yieldxyz/executeTransaction' import { executeTransaction } from '@/lib/yieldxyz/executeTransaction' import type { ActionDto, AugmentedYieldDto, TransactionDto } from '@/lib/yieldxyz/types' import { ActionStatus as YieldActionStatus, TransactionStatus } from '@/lib/yieldxyz/types' -import { formatYieldTxTitle, getDefaultValidatorForYield } from '@/lib/yieldxyz/utils' +import { + formatYieldTxTitle, + getDefaultValidatorForYield, + resolveAssetSymbolForTx, +} from '@/lib/yieldxyz/utils' import { useYieldAccount } from '@/pages/Yields/YieldAccountContext' import { reactQueries } from '@/react-queries' import { useAllowance } from '@/react-queries/hooks/useAllowance' @@ -178,6 +189,12 @@ export const useYieldTransactionFlow = ({ const submitHashMutation = useSubmitYieldTransactionHash() const inputTokenAssetId = useMemo(() => yieldItem?.inputTokens?.[0]?.assetId, [yieldItem]) + const outputTokenSymbol = yieldItem?.outputToken?.symbol + + const resolveSymbolForTx = useCallback( + (txType?: string) => resolveAssetSymbolForTx(txType, action, assetSymbol, outputTokenSymbol), + [action, assetSymbol, outputTokenSymbol], + ) const yieldChainId = yieldItem?.chainId const { accountId: contextAccountId, accountNumber: contextAccountNumber } = useYieldAccount() @@ -353,7 +370,7 @@ export const useYieldTransactionFlow = ({ .map((tx, i) => ({ title: formatYieldTxTitle( tx.title || `Transaction ${i + 1}`, - assetSymbol, + resolveSymbolForTx(tx.type), yieldItem?.mechanics.type, tx.type, ), @@ -368,7 +385,7 @@ export const useYieldTransactionFlow = ({ }, [ transactionSteps, quoteData, - assetSymbol, + resolveSymbolForTx, yieldItem?.mechanics.type, isAllowanceCheckPending, isUsdtResetRequired, @@ -450,7 +467,7 @@ export const useYieldTransactionFlow = ({ typeMessagesMap[actionType] ?? formatYieldTxTitle( tx.title || 'Transaction', - assetSymbol, + resolveSymbolForTx(tx.type), yieldItem.mechanics.type, tx.type, ), @@ -461,7 +478,7 @@ export const useYieldTransactionFlow = ({ }), ) }, - [dispatch, yieldChainId, accountId, action, yieldItem, assetSymbol, amount], + [dispatch, yieldChainId, accountId, action, yieldItem, resolveSymbolForTx, amount], ) const buildCosmosStakeArgs = useCallback((): CosmosStakeArgs | undefined => { @@ -613,6 +630,12 @@ export const useYieldTransactionFlow = ({ queryClient.removeQueries({ queryKey: ['yieldxyz', 'quote'] }) setStep(ModalStep.Success) } else { + const { chainNamespace } = fromChainId(yieldChainId) + if (chainNamespace === CHAIN_NAMESPACE.Evm) { + const publicClient = assertGetViemClient(yieldChainId) + await publicClient.waitForTransactionReceipt({ hash: txHash as Hash }) + } + const confirmedAction = await waitForTransactionConfirmation(actionId, tx.id) const nextTx = confirmedAction.transactions.find( t => t.status === TransactionStatus.Created && t.stepIndex === yieldTxIndex + 1, @@ -775,7 +798,7 @@ export const useYieldTransactionFlow = ({ ...transactions.map((tx, i) => ({ title: formatYieldTxTitle( tx.title || `Transaction ${i + 1}`, - assetSymbol, + resolveSymbolForTx(tx.type), yieldItem?.mechanics.type, tx.type, ), @@ -818,7 +841,7 @@ export const useYieldTransactionFlow = ({ amount, quoteError, quoteData, - assetSymbol, + resolveSymbolForTx, translate, showErrorToast, yieldItem?.mechanics.type,