From 477460efcd9195a63e951417b3af92f51cbcea6c Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Mon, 16 Dec 2024 11:26:19 -0800 Subject: [PATCH 1/3] add buy utils --- src/buy/hooks/useBuyToken.test.ts | 82 ++++++++++ src/buy/hooks/useBuyToken.ts | 34 ++++ src/buy/hooks/useBuyTokens.test.ts | 162 +++++++++++++++++++ src/buy/hooks/useBuyTokens.ts | 46 ++++++ src/buy/hooks/useResetBuyInputs.test.ts | 187 +++++++++++++++++++++ src/buy/hooks/useResetBuyInputs.ts | 27 ++++ src/buy/types.ts | 8 + src/buy/utils/getBuyQuote.test.ts | 207 ++++++++++++++++++++++++ src/buy/utils/getBuyQuote.ts | 71 ++++++++ 9 files changed, 824 insertions(+) create mode 100644 src/buy/hooks/useBuyToken.test.ts create mode 100644 src/buy/hooks/useBuyToken.ts create mode 100644 src/buy/hooks/useBuyTokens.test.ts create mode 100644 src/buy/hooks/useBuyTokens.ts create mode 100644 src/buy/hooks/useResetBuyInputs.test.ts create mode 100644 src/buy/hooks/useResetBuyInputs.ts create mode 100644 src/buy/types.ts create mode 100644 src/buy/utils/getBuyQuote.test.ts create mode 100644 src/buy/utils/getBuyQuote.ts diff --git a/src/buy/hooks/useBuyToken.test.ts b/src/buy/hooks/useBuyToken.test.ts new file mode 100644 index 0000000000..992f149e3d --- /dev/null +++ b/src/buy/hooks/useBuyToken.test.ts @@ -0,0 +1,82 @@ +import { act, renderHook } from '@testing-library/react'; +import { base } from 'viem/chains'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useValue } from '../../core-react/internal/hooks/useValue'; +import type { Token } from '../../token'; +import { usdcToken } from '../../token/constants'; +import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; +import { useBuyToken } from './useBuyToken'; + +vi.mock('../../swap/hooks/useSwapBalances', () => ({ + useSwapBalances: vi.fn(), +})); + +vi.mock('../../core-react/internal/hooks/useValue', () => ({ + useValue: vi.fn(), +})); + +const toToken: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: base.id, +}; + +describe('useBuyToken', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return correct values', () => { + (useSwapBalances as Mock).mockReturnValue({ + fromBalanceString: '100', + fromTokenBalanceError: null, + fromTokenResponse: { refetch: vi.fn() }, + toBalanceString: '200', + toTokenBalanceError: null, + toTokenResponse: { refetch: vi.fn() }, + }); + (useValue as Mock).mockImplementation((props) => ({ + ...props, + })); + const { result } = renderHook(() => + useBuyToken(toToken, usdcToken, '0x123'), + ); + expect(result.current).toEqual({ + amount: '', + amountUSD: '', + balance: '100', + balanceResponse: { refetch: expect.any(Function) }, + error: null, + loading: false, + setAmount: expect.any(Function), + setAmountUSD: expect.any(Function), + setLoading: expect.any(Function), + token: usdcToken, + }); + }); + + it('should call fromTokenResponse.refetch when fromETH.response.refetch is called', async () => { + const mockFromRefetch = vi.fn().mockResolvedValue(undefined); + const mockToRefetch = vi.fn().mockResolvedValue(undefined); + (useSwapBalances as Mock).mockReturnValue({ + fromTokenResponse: { refetch: mockFromRefetch }, + toTokenResponse: { refetch: mockToRefetch }, + }); + (useValue as Mock).mockImplementation((props) => ({ + ...props, + response: props.response, + })); + const { result } = renderHook(() => + useBuyToken(toToken, usdcToken, '0x123'), + ); + await act(async () => { + await result.current.balanceResponse?.refetch(); + }); + expect(mockFromRefetch).toHaveBeenCalledTimes(1); + expect(mockToRefetch).not.toHaveBeenCalled(); + }); +}); diff --git a/src/buy/hooks/useBuyToken.ts b/src/buy/hooks/useBuyToken.ts new file mode 100644 index 0000000000..a90cc78fba --- /dev/null +++ b/src/buy/hooks/useBuyToken.ts @@ -0,0 +1,34 @@ +import { useState } from 'react'; +import type { Address } from 'viem'; +import { useValue } from '../../core-react/internal/hooks/useValue'; +import type { Token } from '../../token'; +import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; + +export const useBuyToken = ( + toToken: Token, + token: Token | undefined, + address: Address | undefined, +) => { + const [amount, setAmount] = useState(''); + const [amountUSD, setAmountUSD] = useState(''); + const [loading, setLoading] = useState(false); + + const { + fromBalanceString: balance, + fromTokenBalanceError: error, + fromTokenResponse: balanceResponse, + } = useSwapBalances({ address, fromToken: token, toToken }); + + return useValue({ + balance, + balanceResponse, + amount, + setAmount, + amountUSD, + setAmountUSD, + token, + loading, + setLoading, + error, + }); +}; diff --git a/src/buy/hooks/useBuyTokens.test.ts b/src/buy/hooks/useBuyTokens.test.ts new file mode 100644 index 0000000000..4b658cb338 --- /dev/null +++ b/src/buy/hooks/useBuyTokens.test.ts @@ -0,0 +1,162 @@ +import { act, renderHook } from '@testing-library/react'; +import { base } from 'viem/chains'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useValue } from '../../core-react/internal/hooks/useValue'; +import type { Token } from '../../token'; +import { + daiToken, + degenToken, + ethToken, + usdcToken, +} from '../../token/constants'; +import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; +import { useBuyToken } from './useBuyToken'; +import { useBuyTokens } from './useBuyTokens'; + +vi.mock('./useBuyToken'); +vi.mock('../../swap/hooks/useSwapBalances'); +vi.mock('../../core-react/internal/hooks/useValue'); + +const toToken: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: base.id, +}; + +const mockFromETH = { + balance: '100', + balanceResponse: { refetch: vi.fn() }, + error: null, + loading: false, + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + setLoading: vi.fn(), + token: ethToken, +}; + +const mockFromUSDC = { + balance: '50', + balanceResponse: { refetch: vi.fn() }, + error: null, + loading: false, + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + setLoading: vi.fn(), + token: usdcToken, +}; + +const mockFrom = { + balance: '50', + balanceResponse: { refetch: vi.fn() }, + error: null, + loading: false, + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + setLoading: vi.fn(), + token: degenToken, +}; + +const mockTo = { + balance: '1000', + balanceResponse: { refetch: vi.fn() }, + error: null, + loading: false, + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + setLoading: vi.fn(), + token: daiToken, +}; + +const address = '0x123'; + +describe('useBuyTokens', () => { + beforeEach(() => { + vi.clearAllMocks(); + (useBuyToken as Mock).mockImplementation((_toToken, fromToken) => { + if (fromToken === ethToken) { + return mockFromETH; + } + if (fromToken === usdcToken) { + return mockFromUSDC; + } + return mockFrom; + }); + + (useSwapBalances as Mock).mockReturnValue({ + toBalanceString: '1000', + toTokenBalanceError: null, + toTokenResponse: { balance: '1000' }, + }); + + (useValue as Mock).mockReturnValue(mockTo); + }); + + it('should return expected swap tokens', () => { + const { result } = renderHook(() => + useBuyTokens(toToken, daiToken, address), + ); + + expect(useBuyToken).toHaveBeenCalledWith(toToken, ethToken, address); + expect(useBuyToken).toHaveBeenCalledWith(toToken, usdcToken, address); + expect(useBuyToken).toHaveBeenCalledWith(toToken, daiToken, address); + expect(useSwapBalances).toHaveBeenCalledWith({ + address, + fromToken: ethToken, + toToken, + }); + expect(useValue).toHaveBeenCalledWith({ + balance: '1000', + balanceResponse: { balance: '1000' }, + amount: '', + setAmount: expect.any(Function), + amountUSD: '', + setAmountUSD: expect.any(Function), + token: toToken, + loading: false, + setLoading: expect.any(Function), + error: null, + }); + + expect(result.current).toEqual({ + fromETH: mockFromETH, + fromUSDC: mockFromUSDC, + from: mockFrom, + to: mockTo, + }); + }); + + it('should handle toToken.symbol === ETH', () => { + renderHook(() => useBuyTokens(ethToken, degenToken, address)); + + expect(useSwapBalances).toHaveBeenCalledWith({ + address, + fromToken: usdcToken, + toToken: ethToken, + }); + }); + + it('should call toTokenResponse.refetch when to.response.refetch is called', async () => { + const mockFromRefetch = vi.fn().mockResolvedValue(undefined); + const mockToRefetch = vi.fn().mockResolvedValue(undefined); + (useSwapBalances as Mock).mockReturnValue({ + fromTokenResponse: { refetch: mockFromRefetch }, + toTokenResponse: { refetch: mockToRefetch }, + }); + (useValue as Mock).mockImplementation((props) => ({ + ...props, + response: props.response, + })); + const { result } = renderHook(() => + useBuyTokens(toToken, undefined, '0x123'), + ); + await act(async () => { + await result.current.to.balanceResponse?.refetch(); + }); + expect(mockToRefetch).toHaveBeenCalledTimes(1); + expect(mockFromRefetch).not.toHaveBeenCalled(); + }); +}); diff --git a/src/buy/hooks/useBuyTokens.ts b/src/buy/hooks/useBuyTokens.ts new file mode 100644 index 0000000000..570b5490c3 --- /dev/null +++ b/src/buy/hooks/useBuyTokens.ts @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import type { Address } from 'viem'; +import { useValue } from '../../core-react/internal/hooks/useValue'; +import type { Token } from '../../token'; +import { ethToken, usdcToken } from '../../token/constants'; +import type { BuyTokens } from '../types'; +import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; +import { useBuyToken } from './useBuyToken'; + +export const useBuyTokens = ( + toToken: Token, + fromToken?: Token, + address?: Address, +): BuyTokens => { + const fromETH = useBuyToken(toToken, ethToken, address); + const fromUSDC = useBuyToken(toToken, usdcToken, address); + const from = useBuyToken(toToken, fromToken, address); + + const [toAmount, setToAmount] = useState(''); + const [toAmountUSD, setToAmountUSD] = useState(''); + const [toLoading, setToLoading] = useState(false); + + // If the toToken is ETH, use USDC for swapQuote + const token = toToken?.symbol === 'ETH' ? usdcToken : ethToken; + + const { + toBalanceString: balance, + toTokenBalanceError: error, + toTokenResponse: balanceResponse, + } = useSwapBalances({ address, fromToken: token, toToken }); + + const to = useValue({ + balance, + balanceResponse, + amount: toAmount, + setAmount: setToAmount, + amountUSD: toAmountUSD, + setAmountUSD: setToAmountUSD, + token: toToken, + loading: toLoading, + setLoading: setToLoading, + error, + }); + + return { fromETH, fromUSDC, from, to }; +}; diff --git a/src/buy/hooks/useResetBuyInputs.test.ts b/src/buy/hooks/useResetBuyInputs.test.ts new file mode 100644 index 0000000000..452cb757ae --- /dev/null +++ b/src/buy/hooks/useResetBuyInputs.test.ts @@ -0,0 +1,187 @@ +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { SwapUnit } from '../../swap/types'; +import { useResetBuyInputs } from './useResetBuyInputs'; + +describe('useResetBuyInputs', () => { + const mockQueryResponse = { + data: undefined, + error: null, + isError: false, + isPending: true, + isSuccess: false, + status: 'pending', + } as const; + + const mockFromTokenResponse = { + ...mockQueryResponse, + refetch: vi.fn().mockResolvedValue(undefined), + }; + const mockFromETHTokenResponse = { + ...mockQueryResponse, + refetch: vi.fn().mockResolvedValue(undefined), + }; + const mockFromUSDCTokenResponse = { + ...mockQueryResponse, + refetch: vi.fn().mockResolvedValue(undefined), + }; + const mockToTokenResponse = { + ...mockQueryResponse, + refetch: vi.fn().mockResolvedValue(undefined), + }; + const mockFrom: SwapUnit = { + balance: '100', + balanceResponse: mockFromTokenResponse, + amount: '50', + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + token: undefined, + loading: false, + setLoading: vi.fn(), + error: undefined, + } as unknown as SwapUnit; + const mockFromETH: SwapUnit = { + balance: '100', + balanceResponse: mockFromETHTokenResponse, + amount: '50', + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + token: undefined, + loading: false, + setLoading: vi.fn(), + error: undefined, + } as unknown as SwapUnit; + const mockFromUSDC: SwapUnit = { + balance: '100', + balanceResponse: mockFromUSDCTokenResponse, + amount: '50', + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + token: undefined, + loading: false, + setLoading: vi.fn(), + error: undefined, + } as unknown as SwapUnit; + const mockTo: SwapUnit = { + balance: '200', + balanceResponse: mockToTokenResponse, + amount: '75', + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + token: undefined, + loading: false, + setLoading: vi.fn(), + error: undefined, + } as unknown as SwapUnit; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return a function', () => { + const { result } = renderHook(() => + useResetBuyInputs({ + fromETH: mockFromETH, + fromUSDC: mockFromUSDC, + from: mockFrom, + to: mockTo, + }), + ); + expect(typeof result.current).toBe('function'); + }); + + it('should call refetch on responses and set amounts to empty strings when executed', async () => { + const { result } = renderHook(() => + useResetBuyInputs({ + fromETH: mockFromETH, + fromUSDC: mockFromUSDC, + from: mockFrom, + to: mockTo, + }), + ); + await act(async () => { + await result.current(); + }); + expect(mockFromETHTokenResponse.refetch).toHaveBeenCalledTimes(1); + expect(mockToTokenResponse.refetch).toHaveBeenCalledTimes(1); + expect(mockFromETH.setAmount).toHaveBeenCalledWith(''); + expect(mockFromETH.setAmountUSD).toHaveBeenCalledWith(''); + expect(mockTo.setAmount).toHaveBeenCalledWith(''); + expect(mockTo.setAmountUSD).toHaveBeenCalledWith(''); + }); + + it("should not create a new function reference if from and to haven't changed", () => { + const { result, rerender } = renderHook(() => + useResetBuyInputs({ + fromETH: mockFromETH, + fromUSDC: mockFromUSDC, + to: mockTo, + }), + ); + const firstRender = result.current; + rerender(); + expect(result.current).toBe(firstRender); + }); + + it('should create a new function reference if from or to change', () => { + const { result, rerender } = renderHook( + ({ fromETH, fromUSDC, to }) => + useResetBuyInputs({ + fromETH, + fromUSDC, + to, + }), + { + initialProps: { + fromETH: mockFromETH, + fromUSDC: mockFromUSDC, + to: mockTo, + }, + }, + ); + const firstRender = result.current; + const newMockFromETH = { + ...mockFromETH, + balanceResponse: { refetch: vi.fn().mockResolvedValue(undefined) }, + } as unknown as SwapUnit; + const newMockFromUSDC = { + ...mockFromUSDC, + balanceResponse: { refetch: vi.fn().mockResolvedValue(undefined) }, + } as unknown as SwapUnit; + rerender({ + fromETH: newMockFromETH, + fromUSDC: newMockFromUSDC, + to: mockTo, + }); + expect(result.current).not.toBe(firstRender); + }); + + it('should handle null responses gracefully', async () => { + const mockFromWithNullResponse = { + ...mockFromETH, + balanceResponse: null, + } as unknown as SwapUnit; + const mockFromUSDCWithNullResponse = { + ...mockFromUSDC, + balanceResponse: null, + } as unknown as SwapUnit; + const mockToWithNullResponse = { + ...mockTo, + balanceResponse: null, + } as unknown as SwapUnit; + const { result } = renderHook(() => + useResetBuyInputs({ + fromETH: mockFromWithNullResponse, + fromUSDC: mockFromUSDCWithNullResponse, + to: mockToWithNullResponse, + }), + ); + await act(async () => { + await result.current(); + }); + expect(mockFromWithNullResponse.setAmount).toHaveBeenCalledWith(''); + expect(mockFromWithNullResponse.setAmountUSD).toHaveBeenCalledWith(''); + expect(mockToWithNullResponse.setAmount).toHaveBeenCalledWith(''); + expect(mockToWithNullResponse.setAmountUSD).toHaveBeenCalledWith(''); + }); +}); diff --git a/src/buy/hooks/useResetBuyInputs.ts b/src/buy/hooks/useResetBuyInputs.ts new file mode 100644 index 0000000000..137ef44f9d --- /dev/null +++ b/src/buy/hooks/useResetBuyInputs.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react'; +import type { BuyTokens } from '../types'; + +// Refreshes balances and inputs post-swap +export const useResetBuyInputs = ({ + fromETH, + fromUSDC, + from, + to, +}: BuyTokens) => { + return useCallback(async () => { + await Promise.all([ + from?.balanceResponse?.refetch(), + from?.setAmount(''), + from?.setAmountUSD(''), + fromETH.balanceResponse?.refetch(), + fromETH.setAmount(''), + fromETH.setAmountUSD(''), + fromUSDC.balanceResponse?.refetch(), + fromUSDC.setAmount(''), + fromUSDC.setAmountUSD(''), + to.balanceResponse?.refetch(), + to.setAmount(''), + to.setAmountUSD(''), + ]); + }, [from, fromETH, fromUSDC, to]); +}; diff --git a/src/buy/types.ts b/src/buy/types.ts new file mode 100644 index 0000000000..1040f9f649 --- /dev/null +++ b/src/buy/types.ts @@ -0,0 +1,8 @@ +import { SwapUnit } from '@/swap/types'; + +export type BuyTokens = { + fromETH: SwapUnit; + fromUSDC: SwapUnit; + to: SwapUnit; + from?: SwapUnit; +}; diff --git a/src/buy/utils/getBuyQuote.test.ts b/src/buy/utils/getBuyQuote.test.ts new file mode 100644 index 0000000000..45aeec94e5 --- /dev/null +++ b/src/buy/utils/getBuyQuote.test.ts @@ -0,0 +1,207 @@ +import { getSwapQuote } from '@/core/api/getSwapQuote'; +import { formatTokenAmount } from '@/core/utils/formatTokenAmount'; +import type { Token } from '@/token/types'; +import { base } from 'viem/chains'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { isSwapError } from '../../swap/utils/isSwapError'; +import { getBuyQuote } from './getBuyQuote'; + +vi.mock('@/core/api/getSwapQuote'); +vi.mock('@/core/utils/formatTokenAmount'); +vi.mock('../../swap/utils/isSwapError'); + +const toToken: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: base.id, +}; + +const fromToken: Token = { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: base.id, +}; + +const mockResponse = { + from: fromToken, + to: toToken, + fromAmount: '100000000000000000', + toAmount: '16732157880511600003860', + amountReference: 'from', + priceImpact: '0.07', + chainId: 8453, + hasHighPriceImpact: false, + slippage: '3', + fromAmountUSD: '100', +}; + +const mockEmptyResponse = { + from: fromToken, + to: toToken, + fromAmount: '', + toAmount: '16732157880511600003860', + amountReference: 'from', + priceImpact: '0.07', + chainId: 8453, + hasHighPriceImpact: false, + slippage: '3', + fromAmountUSD: '', +}; + +const mockFromSwapUnit = { + setAmountUSD: vi.fn(), + setAmount: vi.fn(), + amount: '1', + amountUSD: '1', + loading: false, + setLoading: vi.fn(), + token: fromToken, +}; + +describe('getBuyQuote', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return default values if `from` token is not provided', async () => { + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(result).toEqual({ + response: undefined, + formattedFromAmount: '', + error: undefined, + }); + }); + + it('should call `getSwapQuote` if `from` and `to` tokens are different', async () => { + (getSwapQuote as Mock).mockResolvedValue(mockResponse); + (isSwapError as unknown as Mock).mockReturnValue(false); + (formatTokenAmount as Mock).mockReturnValue('1.0'); + + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(getSwapQuote).toHaveBeenCalledWith({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + }); + + expect(formatTokenAmount).toHaveBeenCalledWith('100000000000000000', 18); + expect(mockFromSwapUnit.setAmountUSD).toHaveBeenCalledWith('100'); + expect(mockFromSwapUnit.setAmount).toHaveBeenCalledWith('1.0'); + + expect(result).toEqual({ + response: mockResponse, + formattedFromAmount: '1.0', + error: undefined, + }); + }); + + it('should handle case where amount values are undefined', async () => { + (getSwapQuote as Mock).mockResolvedValue(mockEmptyResponse); + (isSwapError as unknown as Mock).mockReturnValue(false); + (formatTokenAmount as Mock).mockReturnValue('1.0'); + + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(getSwapQuote).toHaveBeenCalledWith({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + }); + + expect(formatTokenAmount).not.toHaveBeenCalled(); + expect(mockFromSwapUnit.setAmountUSD).toHaveBeenCalledWith(''); + expect(mockFromSwapUnit.setAmount).toHaveBeenCalledWith(''); + + expect(result).toEqual({ + response: mockEmptyResponse, + formattedFromAmount: '', + error: undefined, + }); + }); + + it('should handle swap errors correctly', async () => { + const mockError = { + code: 'UNCAUGHT_SWAP_QUOTE_ERROR', + error: 'Something went wrong', + message: '', + }; + + (getSwapQuote as Mock).mockResolvedValue(mockError); + (isSwapError as unknown as Mock).mockReturnValue(true); + + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(isSwapError).toHaveBeenCalledWith(mockError); + expect(result).toEqual({ + response: undefined, + formattedFromAmount: '', + error: mockError, + }); + }); + + it('should not call `getSwapQuote` if `from` and `to` tokens are the same', async () => { + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: fromToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(getSwapQuote).not.toHaveBeenCalled(); + expect(result).toEqual({ + response: undefined, + formattedFromAmount: '', + error: undefined, + }); + }); +}); diff --git a/src/buy/utils/getBuyQuote.ts b/src/buy/utils/getBuyQuote.ts new file mode 100644 index 0000000000..0234c3dec7 --- /dev/null +++ b/src/buy/utils/getBuyQuote.ts @@ -0,0 +1,71 @@ +import { getSwapQuote } from '@/core/api/getSwapQuote'; +import type { + APIError, + GetSwapQuoteParams, + GetSwapQuoteResponse, +} from '@/core/api/types'; +import { formatTokenAmount } from '@/core/utils/formatTokenAmount'; +import type { SwapError, SwapUnit } from '../../swap/types'; +import { isSwapError } from '../../swap/utils/isSwapError'; +import type { Token } from '../../token'; + +type GetBuyQuoteResponse = { + response?: GetSwapQuoteResponse; + error?: APIError; + formattedFromAmount?: string; +}; + +type GetBuyQuoteParams = Omit & { + fromSwapUnit?: SwapUnit; + from?: Token; +}; + +/** + * Fetches a quote for a swap, but only if the from and to tokens are different. + */ + +export async function getBuyQuote({ + amount, + amountReference, + from, + maxSlippage, + to, + useAggregator, + fromSwapUnit, +}: GetBuyQuoteParams): Promise { + // only fetch quote if the from token is provided + if (!from) { + return { response: undefined, formattedFromAmount: '', error: undefined }; + } + + let response: GetSwapQuoteResponse | undefined; + // only fetch quote if the from and to tokens are different + if (to?.symbol !== from?.symbol) { + response = await getSwapQuote({ + amount, + amountReference, + from, + maxSlippage, + to, + useAggregator, + }); + } + + let formattedFromAmount = ''; + if (response && !isSwapError(response)) { + formattedFromAmount = response?.fromAmount + ? formatTokenAmount(response.fromAmount, response.from.decimals) + : ''; + + fromSwapUnit?.setAmountUSD(response?.fromAmountUSD || ''); + fromSwapUnit?.setAmount(formattedFromAmount || ''); + } + + let error: SwapError | undefined; + if (isSwapError(response)) { + error = response; + response = undefined; + } + + return { response, formattedFromAmount, error }; +} From 70c096b75d7aadb411cef5aa4f5fcfa1b56e8eca Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Mon, 16 Dec 2024 11:28:13 -0800 Subject: [PATCH 2/3] remove old utils --- src/swap/hooks/useResetSwapLiteInputs.test.ts | 187 ---------------- src/swap/hooks/useResetSwapLiteInputs.ts | 27 --- src/swap/hooks/useSwapLiteToken.test.ts | 82 ------- src/swap/hooks/useSwapLiteToken.ts | 34 --- src/swap/hooks/useSwapLiteTokens.test.ts | 162 -------------- src/swap/hooks/useSwapLiteTokens.ts | 46 ---- src/swap/types.ts | 7 - src/swap/utils/getSwapLiteQuote.test.ts | 207 ------------------ src/swap/utils/getSwapLiteQuote.ts | 71 ------ 9 files changed, 823 deletions(-) delete mode 100644 src/swap/hooks/useResetSwapLiteInputs.test.ts delete mode 100644 src/swap/hooks/useResetSwapLiteInputs.ts delete mode 100644 src/swap/hooks/useSwapLiteToken.test.ts delete mode 100644 src/swap/hooks/useSwapLiteToken.ts delete mode 100644 src/swap/hooks/useSwapLiteTokens.test.ts delete mode 100644 src/swap/hooks/useSwapLiteTokens.ts delete mode 100644 src/swap/utils/getSwapLiteQuote.test.ts delete mode 100644 src/swap/utils/getSwapLiteQuote.ts diff --git a/src/swap/hooks/useResetSwapLiteInputs.test.ts b/src/swap/hooks/useResetSwapLiteInputs.test.ts deleted file mode 100644 index c1232384c5..0000000000 --- a/src/swap/hooks/useResetSwapLiteInputs.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { SwapUnit } from '../types'; -import { useResetSwapLiteInputs } from './useResetSwapLiteInputs'; - -describe('useResetSwapLiteInputs', () => { - const mockQueryResponse = { - data: undefined, - error: null, - isError: false, - isPending: true, - isSuccess: false, - status: 'pending', - } as const; - - const mockFromTokenResponse = { - ...mockQueryResponse, - refetch: vi.fn().mockResolvedValue(undefined), - }; - const mockFromETHTokenResponse = { - ...mockQueryResponse, - refetch: vi.fn().mockResolvedValue(undefined), - }; - const mockFromUSDCTokenResponse = { - ...mockQueryResponse, - refetch: vi.fn().mockResolvedValue(undefined), - }; - const mockToTokenResponse = { - ...mockQueryResponse, - refetch: vi.fn().mockResolvedValue(undefined), - }; - const mockFrom: SwapUnit = { - balance: '100', - balanceResponse: mockFromTokenResponse, - amount: '50', - setAmount: vi.fn(), - setAmountUSD: vi.fn(), - token: undefined, - loading: false, - setLoading: vi.fn(), - error: undefined, - } as unknown as SwapUnit; - const mockFromETH: SwapUnit = { - balance: '100', - balanceResponse: mockFromETHTokenResponse, - amount: '50', - setAmount: vi.fn(), - setAmountUSD: vi.fn(), - token: undefined, - loading: false, - setLoading: vi.fn(), - error: undefined, - } as unknown as SwapUnit; - const mockFromUSDC: SwapUnit = { - balance: '100', - balanceResponse: mockFromUSDCTokenResponse, - amount: '50', - setAmount: vi.fn(), - setAmountUSD: vi.fn(), - token: undefined, - loading: false, - setLoading: vi.fn(), - error: undefined, - } as unknown as SwapUnit; - const mockTo: SwapUnit = { - balance: '200', - balanceResponse: mockToTokenResponse, - amount: '75', - setAmount: vi.fn(), - setAmountUSD: vi.fn(), - token: undefined, - loading: false, - setLoading: vi.fn(), - error: undefined, - } as unknown as SwapUnit; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should return a function', () => { - const { result } = renderHook(() => - useResetSwapLiteInputs({ - fromETH: mockFromETH, - fromUSDC: mockFromUSDC, - from: mockFrom, - to: mockTo, - }), - ); - expect(typeof result.current).toBe('function'); - }); - - it('should call refetch on responses and set amounts to empty strings when executed', async () => { - const { result } = renderHook(() => - useResetSwapLiteInputs({ - fromETH: mockFromETH, - fromUSDC: mockFromUSDC, - from: mockFrom, - to: mockTo, - }), - ); - await act(async () => { - await result.current(); - }); - expect(mockFromETHTokenResponse.refetch).toHaveBeenCalledTimes(1); - expect(mockToTokenResponse.refetch).toHaveBeenCalledTimes(1); - expect(mockFromETH.setAmount).toHaveBeenCalledWith(''); - expect(mockFromETH.setAmountUSD).toHaveBeenCalledWith(''); - expect(mockTo.setAmount).toHaveBeenCalledWith(''); - expect(mockTo.setAmountUSD).toHaveBeenCalledWith(''); - }); - - it("should not create a new function reference if from and to haven't changed", () => { - const { result, rerender } = renderHook(() => - useResetSwapLiteInputs({ - fromETH: mockFromETH, - fromUSDC: mockFromUSDC, - to: mockTo, - }), - ); - const firstRender = result.current; - rerender(); - expect(result.current).toBe(firstRender); - }); - - it('should create a new function reference if from or to change', () => { - const { result, rerender } = renderHook( - ({ fromETH, fromUSDC, to }) => - useResetSwapLiteInputs({ - fromETH, - fromUSDC, - to, - }), - { - initialProps: { - fromETH: mockFromETH, - fromUSDC: mockFromUSDC, - to: mockTo, - }, - }, - ); - const firstRender = result.current; - const newMockFromETH = { - ...mockFromETH, - balanceResponse: { refetch: vi.fn().mockResolvedValue(undefined) }, - } as unknown as SwapUnit; - const newMockFromUSDC = { - ...mockFromUSDC, - balanceResponse: { refetch: vi.fn().mockResolvedValue(undefined) }, - } as unknown as SwapUnit; - rerender({ - fromETH: newMockFromETH, - fromUSDC: newMockFromUSDC, - to: mockTo, - }); - expect(result.current).not.toBe(firstRender); - }); - - it('should handle null responses gracefully', async () => { - const mockFromWithNullResponse = { - ...mockFromETH, - balanceResponse: null, - } as unknown as SwapUnit; - const mockFromUSDCWithNullResponse = { - ...mockFromUSDC, - balanceResponse: null, - } as unknown as SwapUnit; - const mockToWithNullResponse = { - ...mockTo, - balanceResponse: null, - } as unknown as SwapUnit; - const { result } = renderHook(() => - useResetSwapLiteInputs({ - fromETH: mockFromWithNullResponse, - fromUSDC: mockFromUSDCWithNullResponse, - to: mockToWithNullResponse, - }), - ); - await act(async () => { - await result.current(); - }); - expect(mockFromWithNullResponse.setAmount).toHaveBeenCalledWith(''); - expect(mockFromWithNullResponse.setAmountUSD).toHaveBeenCalledWith(''); - expect(mockToWithNullResponse.setAmount).toHaveBeenCalledWith(''); - expect(mockToWithNullResponse.setAmountUSD).toHaveBeenCalledWith(''); - }); -}); diff --git a/src/swap/hooks/useResetSwapLiteInputs.ts b/src/swap/hooks/useResetSwapLiteInputs.ts deleted file mode 100644 index af62ad64ce..0000000000 --- a/src/swap/hooks/useResetSwapLiteInputs.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useCallback } from 'react'; -import type { SwapLiteTokens } from '../types'; - -// Refreshes balances and inputs post-swap -export const useResetSwapLiteInputs = ({ - fromETH, - fromUSDC, - from, - to, -}: SwapLiteTokens) => { - return useCallback(async () => { - await Promise.all([ - from?.balanceResponse?.refetch(), - from?.setAmount(''), - from?.setAmountUSD(''), - fromETH.balanceResponse?.refetch(), - fromETH.setAmount(''), - fromETH.setAmountUSD(''), - fromUSDC.balanceResponse?.refetch(), - fromUSDC.setAmount(''), - fromUSDC.setAmountUSD(''), - to.balanceResponse?.refetch(), - to.setAmount(''), - to.setAmountUSD(''), - ]); - }, [from, fromETH, fromUSDC, to]); -}; diff --git a/src/swap/hooks/useSwapLiteToken.test.ts b/src/swap/hooks/useSwapLiteToken.test.ts deleted file mode 100644 index 4d4d6dc9fd..0000000000 --- a/src/swap/hooks/useSwapLiteToken.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { base } from 'viem/chains'; -import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { useValue } from '../../core-react/internal/hooks/useValue'; -import type { Token } from '../../token'; -import { usdcToken } from '../../token/constants'; -import { useSwapBalances } from './useSwapBalances'; -import { useSwapLiteToken } from './useSwapLiteToken'; - -vi.mock('./useSwapBalances', () => ({ - useSwapBalances: vi.fn(), -})); - -vi.mock('../../core-react/internal/hooks/useValue', () => ({ - useValue: vi.fn(), -})); - -const toToken: Token = { - name: 'DEGEN', - address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', - symbol: 'DEGEN', - decimals: 18, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', - chainId: base.id, -}; - -describe('useSwapLiteToken', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should return correct values', () => { - (useSwapBalances as Mock).mockReturnValue({ - fromBalanceString: '100', - fromTokenBalanceError: null, - fromTokenResponse: { refetch: vi.fn() }, - toBalanceString: '200', - toTokenBalanceError: null, - toTokenResponse: { refetch: vi.fn() }, - }); - (useValue as Mock).mockImplementation((props) => ({ - ...props, - })); - const { result } = renderHook(() => - useSwapLiteToken(toToken, usdcToken, '0x123'), - ); - expect(result.current).toEqual({ - amount: '', - amountUSD: '', - balance: '100', - balanceResponse: { refetch: expect.any(Function) }, - error: null, - loading: false, - setAmount: expect.any(Function), - setAmountUSD: expect.any(Function), - setLoading: expect.any(Function), - token: usdcToken, - }); - }); - - it('should call fromTokenResponse.refetch when fromETH.response.refetch is called', async () => { - const mockFromRefetch = vi.fn().mockResolvedValue(undefined); - const mockToRefetch = vi.fn().mockResolvedValue(undefined); - (useSwapBalances as Mock).mockReturnValue({ - fromTokenResponse: { refetch: mockFromRefetch }, - toTokenResponse: { refetch: mockToRefetch }, - }); - (useValue as Mock).mockImplementation((props) => ({ - ...props, - response: props.response, - })); - const { result } = renderHook(() => - useSwapLiteToken(toToken, usdcToken, '0x123'), - ); - await act(async () => { - await result.current.balanceResponse?.refetch(); - }); - expect(mockFromRefetch).toHaveBeenCalledTimes(1); - expect(mockToRefetch).not.toHaveBeenCalled(); - }); -}); diff --git a/src/swap/hooks/useSwapLiteToken.ts b/src/swap/hooks/useSwapLiteToken.ts deleted file mode 100644 index 8c3f620517..0000000000 --- a/src/swap/hooks/useSwapLiteToken.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useState } from 'react'; -import type { Address } from 'viem'; -import { useValue } from '../../core-react/internal/hooks/useValue'; -import type { Token } from '../../token'; -import { useSwapBalances } from './useSwapBalances'; - -export const useSwapLiteToken = ( - toToken: Token, - token: Token | undefined, - address: Address | undefined, -) => { - const [amount, setAmount] = useState(''); - const [amountUSD, setAmountUSD] = useState(''); - const [loading, setLoading] = useState(false); - - const { - fromBalanceString: balance, - fromTokenBalanceError: error, - fromTokenResponse: balanceResponse, - } = useSwapBalances({ address, fromToken: token, toToken }); - - return useValue({ - balance, - balanceResponse, - amount, - setAmount, - amountUSD, - setAmountUSD, - token, - loading, - setLoading, - error, - }); -}; diff --git a/src/swap/hooks/useSwapLiteTokens.test.ts b/src/swap/hooks/useSwapLiteTokens.test.ts deleted file mode 100644 index 7b8280f6f8..0000000000 --- a/src/swap/hooks/useSwapLiteTokens.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { act, renderHook } from '@testing-library/react'; -import { base } from 'viem/chains'; -import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { useValue } from '../../core-react/internal/hooks/useValue'; -import type { Token } from '../../token'; -import { - daiToken, - degenToken, - ethToken, - usdcToken, -} from '../../token/constants'; -import { useSwapBalances } from './useSwapBalances'; -import { useSwapLiteToken } from './useSwapLiteToken'; -import { useSwapLiteTokens } from './useSwapLiteTokens'; - -vi.mock('./useSwapLiteToken'); -vi.mock('./useSwapBalances'); -vi.mock('../../core-react/internal/hooks/useValue'); - -const toToken: Token = { - name: 'DEGEN', - address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', - symbol: 'DEGEN', - decimals: 18, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', - chainId: base.id, -}; - -const mockFromETH = { - balance: '100', - balanceResponse: { refetch: vi.fn() }, - error: null, - loading: false, - setAmount: vi.fn(), - setAmountUSD: vi.fn(), - setLoading: vi.fn(), - token: ethToken, -}; - -const mockFromUSDC = { - balance: '50', - balanceResponse: { refetch: vi.fn() }, - error: null, - loading: false, - setAmount: vi.fn(), - setAmountUSD: vi.fn(), - setLoading: vi.fn(), - token: usdcToken, -}; - -const mockFrom = { - balance: '50', - balanceResponse: { refetch: vi.fn() }, - error: null, - loading: false, - setAmount: vi.fn(), - setAmountUSD: vi.fn(), - setLoading: vi.fn(), - token: degenToken, -}; - -const mockTo = { - balance: '1000', - balanceResponse: { refetch: vi.fn() }, - error: null, - loading: false, - setAmount: vi.fn(), - setAmountUSD: vi.fn(), - setLoading: vi.fn(), - token: daiToken, -}; - -const address = '0x123'; - -describe('useSwapLiteTokens', () => { - beforeEach(() => { - vi.clearAllMocks(); - (useSwapLiteToken as Mock).mockImplementation((_toToken, fromToken) => { - if (fromToken === ethToken) { - return mockFromETH; - } - if (fromToken === usdcToken) { - return mockFromUSDC; - } - return mockFrom; - }); - - (useSwapBalances as Mock).mockReturnValue({ - toBalanceString: '1000', - toTokenBalanceError: null, - toTokenResponse: { balance: '1000' }, - }); - - (useValue as Mock).mockReturnValue(mockTo); - }); - - it('should return expected swap tokens', () => { - const { result } = renderHook(() => - useSwapLiteTokens(toToken, daiToken, address), - ); - - expect(useSwapLiteToken).toHaveBeenCalledWith(toToken, ethToken, address); - expect(useSwapLiteToken).toHaveBeenCalledWith(toToken, usdcToken, address); - expect(useSwapLiteToken).toHaveBeenCalledWith(toToken, daiToken, address); - expect(useSwapBalances).toHaveBeenCalledWith({ - address, - fromToken: ethToken, - toToken, - }); - expect(useValue).toHaveBeenCalledWith({ - balance: '1000', - balanceResponse: { balance: '1000' }, - amount: '', - setAmount: expect.any(Function), - amountUSD: '', - setAmountUSD: expect.any(Function), - token: toToken, - loading: false, - setLoading: expect.any(Function), - error: null, - }); - - expect(result.current).toEqual({ - fromETH: mockFromETH, - fromUSDC: mockFromUSDC, - from: mockFrom, - to: mockTo, - }); - }); - - it('should handle toToken.symbol === ETH', () => { - renderHook(() => useSwapLiteTokens(ethToken, degenToken, address)); - - expect(useSwapBalances).toHaveBeenCalledWith({ - address, - fromToken: usdcToken, - toToken: ethToken, - }); - }); - - it('should call toTokenResponse.refetch when to.response.refetch is called', async () => { - const mockFromRefetch = vi.fn().mockResolvedValue(undefined); - const mockToRefetch = vi.fn().mockResolvedValue(undefined); - (useSwapBalances as Mock).mockReturnValue({ - fromTokenResponse: { refetch: mockFromRefetch }, - toTokenResponse: { refetch: mockToRefetch }, - }); - (useValue as Mock).mockImplementation((props) => ({ - ...props, - response: props.response, - })); - const { result } = renderHook(() => - useSwapLiteTokens(toToken, undefined, '0x123'), - ); - await act(async () => { - await result.current.to.balanceResponse?.refetch(); - }); - expect(mockToRefetch).toHaveBeenCalledTimes(1); - expect(mockFromRefetch).not.toHaveBeenCalled(); - }); -}); diff --git a/src/swap/hooks/useSwapLiteTokens.ts b/src/swap/hooks/useSwapLiteTokens.ts deleted file mode 100644 index e364df6faf..0000000000 --- a/src/swap/hooks/useSwapLiteTokens.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useState } from 'react'; -import type { Address } from 'viem'; -import { useValue } from '../../core-react/internal/hooks/useValue'; -import type { Token } from '../../token'; -import { ethToken, usdcToken } from '../../token/constants'; -import type { SwapLiteTokens } from '../types'; -import { useSwapBalances } from './useSwapBalances'; -import { useSwapLiteToken } from './useSwapLiteToken'; - -export const useSwapLiteTokens = ( - toToken: Token, - fromToken?: Token, - address?: Address, -): SwapLiteTokens => { - const fromETH = useSwapLiteToken(toToken, ethToken, address); - const fromUSDC = useSwapLiteToken(toToken, usdcToken, address); - const from = useSwapLiteToken(toToken, fromToken, address); - - const [toAmount, setToAmount] = useState(''); - const [toAmountUSD, setToAmountUSD] = useState(''); - const [toLoading, setToLoading] = useState(false); - - // If the toToken is ETH, use USDC for swapQuote - const token = toToken?.symbol === 'ETH' ? usdcToken : ethToken; - - const { - toBalanceString: balance, - toTokenBalanceError: error, - toTokenResponse: balanceResponse, - } = useSwapBalances({ address, fromToken: token, toToken }); - - const to = useValue({ - balance, - balanceResponse, - amount: toAmount, - setAmount: setToAmount, - amountUSD: toAmountUSD, - setAmountUSD: setToAmountUSD, - token: toToken, - loading: toLoading, - setLoading: setToLoading, - error, - }); - - return { fromETH, fromUSDC, from, to }; -}; diff --git a/src/swap/types.ts b/src/swap/types.ts index 44436dbcfc..cc9dd656bc 100644 --- a/src/swap/types.ts +++ b/src/swap/types.ts @@ -173,13 +173,6 @@ export type ProcessSwapTransactionParams = { walletCapabilities: WalletCapabilities; // EIP-5792 wallet capabilities }; -export type SwapLiteTokens = { - fromETH: SwapUnit; - fromUSDC: SwapUnit; - to: SwapUnit; - from?: SwapUnit; -}; - /** * Note: exported as public Type */ diff --git a/src/swap/utils/getSwapLiteQuote.test.ts b/src/swap/utils/getSwapLiteQuote.test.ts deleted file mode 100644 index b31ec306e2..0000000000 --- a/src/swap/utils/getSwapLiteQuote.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { getSwapQuote } from '@/core/api/getSwapQuote'; -import { formatTokenAmount } from '@/core/utils/formatTokenAmount'; -import type { Token } from '@/token/types'; -import { base } from 'viem/chains'; -import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { isSwapError } from '../../swap/utils/isSwapError'; -import { getSwapLiteQuote } from './getSwapLiteQuote'; - -vi.mock('@/core/api/getSwapQuote'); -vi.mock('@/core/utils/formatTokenAmount'); -vi.mock('../../swap/utils/isSwapError'); - -const toToken: Token = { - name: 'DEGEN', - address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', - symbol: 'DEGEN', - decimals: 18, - image: - 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', - chainId: base.id, -}; - -const fromToken: Token = { - name: 'ETH', - address: '', - symbol: 'ETH', - decimals: 18, - image: - 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', - chainId: base.id, -}; - -const mockResponse = { - from: fromToken, - to: toToken, - fromAmount: '100000000000000000', - toAmount: '16732157880511600003860', - amountReference: 'from', - priceImpact: '0.07', - chainId: 8453, - hasHighPriceImpact: false, - slippage: '3', - fromAmountUSD: '100', -}; - -const mockEmptyResponse = { - from: fromToken, - to: toToken, - fromAmount: '', - toAmount: '16732157880511600003860', - amountReference: 'from', - priceImpact: '0.07', - chainId: 8453, - hasHighPriceImpact: false, - slippage: '3', - fromAmountUSD: '', -}; - -const mockFromSwapUnit = { - setAmountUSD: vi.fn(), - setAmount: vi.fn(), - amount: '1', - amountUSD: '1', - loading: false, - setLoading: vi.fn(), - token: fromToken, -}; - -describe('getSwapLiteQuote', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should return default values if `from` token is not provided', async () => { - const result = await getSwapLiteQuote({ - amount: '1', - amountReference: 'exactIn', - maxSlippage: '0.5', - to: toToken, - useAggregator: true, - fromSwapUnit: mockFromSwapUnit, - }); - - expect(result).toEqual({ - response: undefined, - formattedFromAmount: '', - error: undefined, - }); - }); - - it('should call `getSwapQuote` if `from` and `to` tokens are different', async () => { - (getSwapQuote as Mock).mockResolvedValue(mockResponse); - (isSwapError as unknown as Mock).mockReturnValue(false); - (formatTokenAmount as Mock).mockReturnValue('1.0'); - - const result = await getSwapLiteQuote({ - amount: '1', - amountReference: 'exactIn', - from: fromToken, - maxSlippage: '0.5', - to: toToken, - useAggregator: true, - fromSwapUnit: mockFromSwapUnit, - }); - - expect(getSwapQuote).toHaveBeenCalledWith({ - amount: '1', - amountReference: 'exactIn', - from: fromToken, - maxSlippage: '0.5', - to: toToken, - useAggregator: true, - }); - - expect(formatTokenAmount).toHaveBeenCalledWith('100000000000000000', 18); - expect(mockFromSwapUnit.setAmountUSD).toHaveBeenCalledWith('100'); - expect(mockFromSwapUnit.setAmount).toHaveBeenCalledWith('1.0'); - - expect(result).toEqual({ - response: mockResponse, - formattedFromAmount: '1.0', - error: undefined, - }); - }); - - it('should handle case where amount values are undefined', async () => { - (getSwapQuote as Mock).mockResolvedValue(mockEmptyResponse); - (isSwapError as unknown as Mock).mockReturnValue(false); - (formatTokenAmount as Mock).mockReturnValue('1.0'); - - const result = await getSwapLiteQuote({ - amount: '1', - amountReference: 'exactIn', - from: fromToken, - maxSlippage: '0.5', - to: toToken, - useAggregator: true, - fromSwapUnit: mockFromSwapUnit, - }); - - expect(getSwapQuote).toHaveBeenCalledWith({ - amount: '1', - amountReference: 'exactIn', - from: fromToken, - maxSlippage: '0.5', - to: toToken, - useAggregator: true, - }); - - expect(formatTokenAmount).not.toHaveBeenCalled(); - expect(mockFromSwapUnit.setAmountUSD).toHaveBeenCalledWith(''); - expect(mockFromSwapUnit.setAmount).toHaveBeenCalledWith(''); - - expect(result).toEqual({ - response: mockEmptyResponse, - formattedFromAmount: '', - error: undefined, - }); - }); - - it('should handle swap errors correctly', async () => { - const mockError = { - code: 'UNCAUGHT_SWAP_QUOTE_ERROR', - error: 'Something went wrong', - message: '', - }; - - (getSwapQuote as Mock).mockResolvedValue(mockError); - (isSwapError as unknown as Mock).mockReturnValue(true); - - const result = await getSwapLiteQuote({ - amount: '1', - amountReference: 'exactIn', - from: fromToken, - maxSlippage: '0.5', - to: toToken, - useAggregator: true, - fromSwapUnit: mockFromSwapUnit, - }); - - expect(isSwapError).toHaveBeenCalledWith(mockError); - expect(result).toEqual({ - response: undefined, - formattedFromAmount: '', - error: mockError, - }); - }); - - it('should not call `getSwapQuote` if `from` and `to` tokens are the same', async () => { - const result = await getSwapLiteQuote({ - amount: '1', - amountReference: 'exactIn', - from: fromToken, - maxSlippage: '0.5', - to: fromToken, - useAggregator: true, - fromSwapUnit: mockFromSwapUnit, - }); - - expect(getSwapQuote).not.toHaveBeenCalled(); - expect(result).toEqual({ - response: undefined, - formattedFromAmount: '', - error: undefined, - }); - }); -}); diff --git a/src/swap/utils/getSwapLiteQuote.ts b/src/swap/utils/getSwapLiteQuote.ts deleted file mode 100644 index cd1009a76d..0000000000 --- a/src/swap/utils/getSwapLiteQuote.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { getSwapQuote } from '@/core/api/getSwapQuote'; -import type { - APIError, - GetSwapQuoteParams, - GetSwapQuoteResponse, -} from '@/core/api/types'; -import { formatTokenAmount } from '@/core/utils/formatTokenAmount'; -import type { SwapError, SwapUnit } from '../../swap/types'; -import { isSwapError } from '../../swap/utils/isSwapError'; -import type { Token } from '../../token'; - -type GetSwapLiteQuoteResponse = { - response?: GetSwapQuoteResponse; - error?: APIError; - formattedFromAmount?: string; -}; - -type GetSwapLiteQuoteParams = Omit & { - fromSwapUnit: SwapUnit; - from?: Token; -}; - -/** - * Fetches a quote for a swap, but only if the from and to tokens are different. - */ - -export async function getSwapLiteQuote({ - amount, - amountReference, - from, - maxSlippage, - to, - useAggregator, - fromSwapUnit, -}: GetSwapLiteQuoteParams): Promise { - // only fetch quote if the from token is provided - if (!from) { - return { response: undefined, formattedFromAmount: '', error: undefined }; - } - - let response: GetSwapQuoteResponse | undefined; - // only fetch quote if the from and to tokens are different - if (to?.symbol !== from?.symbol) { - response = await getSwapQuote({ - amount, - amountReference, - from, - maxSlippage, - to, - useAggregator, - }); - } - - let formattedFromAmount = ''; - if (response && !isSwapError(response)) { - formattedFromAmount = response?.fromAmount - ? formatTokenAmount(response.fromAmount, response.from.decimals) - : ''; - - fromSwapUnit.setAmountUSD(response?.fromAmountUSD || ''); - fromSwapUnit.setAmount(formattedFromAmount || ''); - } - - let error: SwapError | undefined; - if (isSwapError(response)) { - error = response; - response = undefined; - } - - return { response, formattedFromAmount, error }; -} From 060376cb077673f9021fe9d811c99d256a787f61 Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Mon, 16 Dec 2024 11:34:22 -0800 Subject: [PATCH 3/3] fix lint --- src/buy/hooks/useBuyToken.test.ts | 2 +- src/buy/hooks/useBuyToken.ts | 2 +- src/buy/hooks/useBuyTokens.test.ts | 2 +- src/buy/hooks/useBuyTokens.ts | 2 +- src/buy/types.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/buy/hooks/useBuyToken.test.ts b/src/buy/hooks/useBuyToken.test.ts index 992f149e3d..f229d41a4c 100644 --- a/src/buy/hooks/useBuyToken.test.ts +++ b/src/buy/hooks/useBuyToken.test.ts @@ -2,9 +2,9 @@ import { act, renderHook } from '@testing-library/react'; import { base } from 'viem/chains'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { useValue } from '../../core-react/internal/hooks/useValue'; +import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; import type { Token } from '../../token'; import { usdcToken } from '../../token/constants'; -import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; import { useBuyToken } from './useBuyToken'; vi.mock('../../swap/hooks/useSwapBalances', () => ({ diff --git a/src/buy/hooks/useBuyToken.ts b/src/buy/hooks/useBuyToken.ts index a90cc78fba..7be9f07f28 100644 --- a/src/buy/hooks/useBuyToken.ts +++ b/src/buy/hooks/useBuyToken.ts @@ -1,8 +1,8 @@ import { useState } from 'react'; import type { Address } from 'viem'; import { useValue } from '../../core-react/internal/hooks/useValue'; -import type { Token } from '../../token'; import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; +import type { Token } from '../../token'; export const useBuyToken = ( toToken: Token, diff --git a/src/buy/hooks/useBuyTokens.test.ts b/src/buy/hooks/useBuyTokens.test.ts index 4b658cb338..5da700332f 100644 --- a/src/buy/hooks/useBuyTokens.test.ts +++ b/src/buy/hooks/useBuyTokens.test.ts @@ -2,6 +2,7 @@ import { act, renderHook } from '@testing-library/react'; import { base } from 'viem/chains'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { useValue } from '../../core-react/internal/hooks/useValue'; +import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; import type { Token } from '../../token'; import { daiToken, @@ -9,7 +10,6 @@ import { ethToken, usdcToken, } from '../../token/constants'; -import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; import { useBuyToken } from './useBuyToken'; import { useBuyTokens } from './useBuyTokens'; diff --git a/src/buy/hooks/useBuyTokens.ts b/src/buy/hooks/useBuyTokens.ts index 570b5490c3..9a09159877 100644 --- a/src/buy/hooks/useBuyTokens.ts +++ b/src/buy/hooks/useBuyTokens.ts @@ -1,10 +1,10 @@ import { useState } from 'react'; import type { Address } from 'viem'; import { useValue } from '../../core-react/internal/hooks/useValue'; +import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; import type { Token } from '../../token'; import { ethToken, usdcToken } from '../../token/constants'; import type { BuyTokens } from '../types'; -import { useSwapBalances } from '../../swap/hooks/useSwapBalances'; import { useBuyToken } from './useBuyToken'; export const useBuyTokens = ( diff --git a/src/buy/types.ts b/src/buy/types.ts index 1040f9f649..ac853e565e 100644 --- a/src/buy/types.ts +++ b/src/buy/types.ts @@ -1,4 +1,4 @@ -import { SwapUnit } from '@/swap/types'; +import type { SwapUnit } from '@/swap/types'; export type BuyTokens = { fromETH: SwapUnit;