Skip to content

Commit

Permalink
feat: Add SwapLite util functions (#1648)
Browse files Browse the repository at this point in the history
Co-authored-by: Alissa Crane <[email protected]>
  • Loading branch information
abcrane123 and alissacrane-cb authored Dec 16, 2024
1 parent 077e5e6 commit 2a490b3
Show file tree
Hide file tree
Showing 13 changed files with 876 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/swap/components/SwapAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function SwapAmountInput({
const destination = useValue(type === 'from' ? to : from);
useEffect(() => {
if (token) {
source.setToken(token);
source.setToken?.(token);
}
}, [token, source.setToken]);

Expand All @@ -52,7 +52,7 @@ export function SwapAmountInput({

const handleSetToken = useCallback(
(token: Token) => {
source.setToken(token);
source.setToken?.(token);
handleAmountChange(type, source.amount, token);
},
[source.amount, source.setToken, handleAmountChange, type],
Expand Down
12 changes: 6 additions & 6 deletions src/swap/components/SwapProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ const renderWithProviders = ({
const TestSwapComponent = () => {
const context = useSwapContext();
useEffect(() => {
context.from.setToken(ETH_TOKEN);
context.from.setToken?.(ETH_TOKEN);
context.from.setAmount('100');
context.to.setToken(DEGEN_TOKEN);
context.to.setToken?.(DEGEN_TOKEN);
}, [context]);
const handleStatusError = async () => {
context.updateLifecycleStatus({
Expand Down Expand Up @@ -555,8 +555,8 @@ describe('SwapProvider', () => {
React.useEffect(() => {
const initializeSwap = async () => {
await act(async () => {
from.setToken(ETH_TOKEN);
to.setToken(DEGEN_TOKEN);
from.setToken?.(ETH_TOKEN);
to.setToken?.(DEGEN_TOKEN);
handleToggle();
});
};
Expand Down Expand Up @@ -652,9 +652,9 @@ describe('SwapProvider', () => {
it('should toggle tokens and amounts', async () => {
const { result } = renderHook(() => useSwapContext(), { wrapper });
await act(async () => {
result.current.from.setToken(ETH_TOKEN);
result.current.from.setToken?.(ETH_TOKEN);
result.current.from.setAmount('10');
result.current.to.setToken(DEGEN_TOKEN);
result.current.to.setToken?.(DEGEN_TOKEN);
result.current.to.setAmount('1000');
});
await act(async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/swap/components/SwapProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ export function SwapProvider({
const handleToggle = useCallback(() => {
from.setAmount(to.amount);
to.setAmount(from.amount);
from.setToken(to.token);
to.setToken(from.token);
from.setToken?.(to.token);
to.setToken?.(from.token);

updateLifecycleStatus({
statusName: 'amountChange',
Expand Down
187 changes: 187 additions & 0 deletions src/swap/hooks/useResetSwapLiteInputs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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('');
});
});
27 changes: 27 additions & 0 deletions src/swap/hooks/useResetSwapLiteInputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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]);
};
82 changes: 82 additions & 0 deletions src/swap/hooks/useSwapLiteToken.test.ts
Original file line number Diff line number Diff line change
@@ -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 './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();
});
});
34 changes: 34 additions & 0 deletions src/swap/hooks/useSwapLiteToken.ts
Original file line number Diff line number Diff line change
@@ -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 './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,
});
};
Loading

0 comments on commit 2a490b3

Please sign in to comment.