Skip to content

Commit

Permalink
feat: Add Swap USD values (#1286)
Browse files Browse the repository at this point in the history
  • Loading branch information
cpcramer authored Sep 24, 2024
1 parent 32f9726 commit 4361849
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-hairs-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@coinbase/onchainkit': patch
---

feat: Add Swap USD values. By @cpcramer #1286
82 changes: 48 additions & 34 deletions src/swap/components/SwapAmountInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import {
type Mock,
type MockedFunction,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Token } from '../../token';
import { DAI_TOKEN, ETH_TOKEN, USDC_TOKEN } from '../mocks';
import type { SwapContextType } from '../types';
Expand Down Expand Up @@ -39,12 +30,15 @@ vi.mock('./SwapProvider', () => ({
useSwapContext: vi.fn(),
}));

const useSwapContextMock = useSwapContext as Mock;
const useSwapContextMock = useSwapContext as unknown as ReturnType<
typeof vi.fn
>;

const mockContextValue = {
address: '0x123',
from: {
amount: '10',
amountUSD: '1000',
balance: '0.0002851826238227',
setAmount: vi.fn(),
setLoading: vi.fn(),
Expand All @@ -54,6 +48,7 @@ const mockContextValue = {
},
to: {
amount: '20',
amountUSD: '2000',
setAmount: vi.fn(),
setLoading: vi.fn(),
setToken: vi.fn(),
Expand All @@ -66,6 +61,10 @@ const mockContextValue = {
handleAmountChange: vi.fn(),
} as SwapContextType;

vi.mock('../../internal/utils/getRoundedAmount', () => ({
getRoundedAmount: vi.fn((value) => value.slice(0, 10)),
}));

const mockSwappableTokens: Token[] = [ETH_TOKEN, USDC_TOKEN, DAI_TOKEN];

describe('SwapAmountInput', () => {
Expand All @@ -76,32 +75,26 @@ describe('SwapAmountInput', () => {
it('should render the component with the correct label and token', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="from" />);
expect(screen.getByText('From')).toBeInTheDocument();
expect(screen.getByText('From')).toBeDefined();
});

it('should render from token input with max button and balance', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="from" />);
expect(screen.getByText('Balance: 0.00028518')).toBeInTheDocument();
expect(
screen.getByTestId('ockSwapAmountInput_MaxButton'),
).toBeInTheDocument();
expect(screen.getByText('Balance: 0.00028518')).toBeDefined();
expect(screen.getByTestId('ockSwapAmountInput_MaxButton')).toBeDefined();
});

it('should not render max button for to token input', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="to" />);
expect(
screen.queryByTestId('ockSwapAmountInput_MaxButton'),
).not.toBeInTheDocument();
expect(screen.queryByTestId('ockSwapAmountInput_MaxButton')).toBeNull();
});

it('should not render max button if wallet not connected', () => {
useSwapContextMock.mockReturnValue({ ...mockContextValue, address: '' });
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="from" />);
expect(
screen.queryByTestId('ockSwapAmountInput_MaxButton'),
).not.toBeInTheDocument();
expect(screen.queryByTestId('ockSwapAmountInput_MaxButton')).toBeNull();
});

it('should update input value with balance amount on max button click', () => {
Expand Down Expand Up @@ -129,7 +122,7 @@ describe('SwapAmountInput', () => {
expect(mockContextValue.from.setAmount).not.toHaveBeenCalled();
});

it('shoukd display the correct amount when this type is "from"', () => {
it('should display the correct amount when this type is "from"', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="from" />);
const input = screen.getByTestId('ockTextInput_Input');
Expand Down Expand Up @@ -179,7 +172,7 @@ describe('SwapAmountInput', () => {
expect(mockContextValue.to.setToken).toHaveBeenCalledWith(ETH_TOKEN);
});

it('should call handleAmountChange when type is "from" and delayMs is 0', () => {
it('should call handleAmountChange when type is "from" and delayMs is 0', async () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(
<SwapAmountInput
Expand All @@ -192,7 +185,7 @@ describe('SwapAmountInput', () => {
const input = screen.getByTestId('ockTextInput_Input');
fireEvent.change(input, { target: { value: '15' } });
expect(mockContextValue.from.setAmount).toHaveBeenCalledWith('15');
waitFor(() => {
await waitFor(() => {
expect(mockContextValue.handleAmountChange).toHaveBeenCalled();
});
});
Expand All @@ -215,13 +208,10 @@ describe('SwapAmountInput', () => {
/>,
);
const dropdown = screen.getByText(/TokenSelectDropdown/i);
expect(dropdown).toBeInTheDocument();
expect(dropdown).toBeDefined();
});

it('should correctly select a token from the dropdown using mouse and keyboard', () => {
const useSwapContextMock = useSwapContext as MockedFunction<
typeof useSwapContext
>;
useSwapContextMock.mockReturnValue(mockContextValue);
render(
<SwapAmountInput
Expand Down Expand Up @@ -264,7 +254,7 @@ describe('SwapAmountInput', () => {
useSwapContextMock.mockReturnValue(mockContextValueWithLowBalance);
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="from" />);
const input = screen.getByTestId('ockTextInput_Input');
expect(input).toHaveClass('text-ock-error');
expect(input.className).toContain('text-ock-error');
});

it('should render a TokenChip component if swappableTokens are not passed as prop', () => {
Expand All @@ -278,7 +268,7 @@ describe('SwapAmountInput', () => {
render(<SwapAmountInput label="To" token={USDC_TOKEN} type="to" />);
const chips = screen.getAllByText('TokenChip');
expect(chips.length).toBeGreaterThan(0);
expect(chips[0]).toBeInTheDocument();
expect(chips[0]).toBeDefined();
});

it('should apply the given className to the button', async () => {
Expand All @@ -291,8 +281,32 @@ describe('SwapAmountInput', () => {
className="custom-class"
/>,
);
expect(screen.getByTestId('ockSwapAmountInput_Container')).toHaveClass(
'custom-class',
);
expect(
screen.getByTestId('ockSwapAmountInput_Container').className,
).toContain('custom-class');
});

it('should not display anything when amountUSD is null', () => {
const mockContextValueWithNullUSD = {
...mockContextValue,
from: {
...mockContextValue.from,
amountUSD: null,
},
};
useSwapContextMock.mockReturnValue(mockContextValueWithNullUSD);
expect(screen.queryByText(/\$/)).toBeNull();
});

it('should return null when amount is falsy', () => {
useSwapContextMock.mockReturnValue({
...mockContextValue,
from: {
...mockContextValue.from,
amountUSD: '',
},
});
render(<SwapAmountInput label="From" token={ETH_TOKEN} type="from" />);
expect(screen.queryByText(/\$/)).toBeNull();
});
});
14 changes: 13 additions & 1 deletion src/swap/components/SwapAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export function SwapAmountInput({

const source = useValue(type === 'from' ? from : to);
const destination = useValue(type === 'from' ? to : from);

useEffect(() => {
if (token) {
source.setToken(token);
Expand Down Expand Up @@ -64,6 +63,14 @@ export function SwapAmountInput({
const hasInsufficientBalance =
type === 'from' && Number(source.balance) < Number(source.amount);

const formatUSD = (amount: string) => {
if (!amount) {
return null;
}
const roundedAmount = Number(getRoundedAmount(amount, 2));
return `~$${roundedAmount.toFixed(2)}`;
};

return (
<div
className={cn(
Expand Down Expand Up @@ -105,6 +112,11 @@ export function SwapAmountInput({
)}
</div>
<div className="mt-4 flex w-full justify-between">
<div className="flex items-center">
<span className={cn(text.label2, color.foregroundMuted)}>
{formatUSD(source.amountUSD)}
</span>
</div>
<span className={cn(text.label2, color.foregroundMuted)}>{''}</span>
<div className="flex items-center">
{source.balance && (
Expand Down
7 changes: 6 additions & 1 deletion src/swap/components/SwapProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ export function SwapProvider({
return;
}
if (amount === '' || amount === '.' || Number.parseFloat(amount) === 0) {
return destination.setAmount('');
destination.setAmount('');
destination.setAmountUSD('');
source.setAmountUSD('');
return;
}

// When toAmount changes we fetch quote for fromAmount
Expand Down Expand Up @@ -272,7 +275,9 @@ export function SwapProvider({
response.toAmount,
response.to.decimals,
);
destination.setAmountUSD(response.toAmountUSD);
destination.setAmount(formattedAmount);
source.setAmountUSD(response.fromAmountUSD);
updateLifecycleStatus({
statusName: 'amountChange',
statusData: {
Expand Down
6 changes: 6 additions & 0 deletions src/swap/hooks/useFromTo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,37 @@ describe('useFromTo', () => {
(useValue as Mock).mockImplementation((props) => ({
...props,
amount: '100',
amountUSD: '150',
response: props.response,
setAmount: vi.fn(),
setAmountUSD: vi.fn(),
setLoading: vi.fn(),
setToken: vi.fn(),
token: USDC_TOKEN,
}));
const { result } = renderHook(() => useFromTo('0x123'));
expect(result.current.from).toEqual({
amount: '100',
amountUSD: '150',
balance: '100',
balanceResponse: { refetch: expect.any(Function) },
error: null,
loading: false,
setAmount: expect.any(Function),
setAmountUSD: expect.any(Function),
setLoading: expect.any(Function),
setToken: expect.any(Function),
token: USDC_TOKEN,
});
expect(result.current.to).toEqual({
amount: '100',
amountUSD: '150',
balance: '200',
balanceResponse: { refetch: expect.any(Function) },
error: null,
loading: false,
setAmount: expect.any(Function),
setAmountUSD: expect.any(Function),
setLoading: expect.any(Function),
setToken: expect.any(Function),
token: USDC_TOKEN,
Expand Down
6 changes: 6 additions & 0 deletions src/swap/hooks/useFromTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { useSwapBalances } from './useSwapBalances';

export const useFromTo = (address?: Address): FromTo => {
const [fromAmount, setFromAmount] = useState('');
const [fromAmountUSD, setFromAmountUSD] = useState('');
const [fromToken, setFromToken] = useState<Token>();
const [toAmount, setToAmount] = useState('');
const [toAmountUSD, setToAmountUSD] = useState('');
const [toToken, setToToken] = useState<Token>();
const [toLoading, setToLoading] = useState(false);
const [fromLoading, setFromLoading] = useState(false);
Expand All @@ -27,6 +29,8 @@ export const useFromTo = (address?: Address): FromTo => {
balanceResponse: fromTokenResponse,
amount: fromAmount,
setAmount: setFromAmount,
amountUSD: fromAmountUSD,
setAmountUSD: setFromAmountUSD,
token: fromToken,
setToken: setFromToken,
loading: fromLoading,
Expand All @@ -38,6 +42,8 @@ export const useFromTo = (address?: Address): FromTo => {
balance: toBalanceString,
balanceResponse: toTokenResponse,
amount: toAmount,
amountUSD: toAmountUSD,
setAmountUSD: setToAmountUSD,
setAmount: setToAmount,
token: toToken,
setToken: setToToken,
Expand Down
4 changes: 4 additions & 0 deletions src/swap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,13 @@ export type SwapQuote = {
amountReference: string; // The reference amount for the quote
from: Token; // The source token for the swap
fromAmount: string; // The amount of the source token
fromAmountUSD: string; // The USD value of the source token
hasHighPriceImpact: boolean; // Whether the price impact is high
priceImpact: string; // The price impact of the swap
slippage: string; // The slippage of the swap
to: Token; // The destination token for the swap
toAmount: string; // The amount of the destination token
toAmountUSD: string; // The USD value of the destination token
warning?: QuoteWarning; // The warning associated with the quote
};

Expand Down Expand Up @@ -338,11 +340,13 @@ export type SwapTransactionType = 'Batched' | 'ERC20' | 'Permit2' | 'Swap'; // C

export type SwapUnit = {
amount: string;
amountUSD: string;
balance?: string;
balanceResponse?: UseBalanceReturnType | UseReadContractReturnType;
error?: SwapError;
loading: boolean;
setAmount: Dispatch<SetStateAction<string>>;
setAmountUSD: Dispatch<SetStateAction<string>>;
setLoading: Dispatch<SetStateAction<boolean>>;
setToken: Dispatch<SetStateAction<Token | undefined>>;
token: Token | undefined;
Expand Down

0 comments on commit 4361849

Please sign in to comment.