Skip to content

Commit

Permalink
feat: added swappable tokens to SwapAmountInput (coinbase#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
abcrane123 authored Jun 14, 2024
1 parent b22f69b commit 1b1dbe5
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 5 deletions.
1 change: 1 addition & 0 deletions site/docs/pages/swap/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type GetSwapQuoteResponse = SwapQuote | SwapError;
```ts
type SwapAmountInputReact = {
label: string; // Descriptive label for the input field
swappableTokens?: Token[] // Swappable tokens
token?: Token; // Selected token
type: 'to' | 'from'; // Identifies if component is for toToken or fromToken
};
Expand Down
76 changes: 76 additions & 0 deletions src/swap/components/SwapAmountInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Address } from 'viem';

jest.mock('../../token', () => ({
TokenChip: jest.fn(() => <div>TokenChip</div>),
TokenSelectDropdown: jest.fn(() => <div>TokenSelectDropdown</div>),
}));

jest.mock('wagmi', () => {
Expand All @@ -28,6 +29,24 @@ const mockContextValue = {
setToToken: jest.fn(),
toAmount: '20',
address: '0x5FbDB2315678afecb367f032d93F642f64180aa3' as Address,
toToken: {
name: 'Ethereum',
address: '',
symbol: 'ETH',
decimals: 18,
image:
'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
chainId: 8453,
},
fromToken: {
name: 'USDC',
address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
symbol: 'USDC',
decimals: 6,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2',
chainId: 8453,
},
} as SwapContextType;

const mockToken: Token = {
Expand All @@ -47,6 +66,36 @@ const mockBalance = {
value: 285182623822700n,
};

const mockSwappableTokens: Token[] = [
{
name: 'Ethereum',
address: '',
symbol: 'ETH',
decimals: 18,
image:
'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
chainId: 8453,
},
{
name: 'USDC',
address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
symbol: 'USDC',
decimals: 6,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2',
chainId: 8453,
},
{
name: 'Dai',
address: '0x50c5725949a6f0c72e6c4a641f24049a917db0cb',
symbol: 'DAI',
decimals: 18,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/d0/d7/d0d7784975771dbbac9a22c8c0c12928cc6f658cbcf2bbbf7c909f0fa2426dec-NmU4ZWViMDItOTQyYy00Yjk5LTkzODUtNGJlZmJiMTUxOTgy',
chainId: 8453,
},
];

describe('SwapAmountInput', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -192,4 +241,31 @@ describe('SwapAmountInput', () => {

expect(mockContextValue.setToToken).toHaveBeenCalledWith(mockToken);
});

it('renders a TokenSelectDropdown component if swappableTokens are passed as prop', () => {
render(
<SwapContext.Provider value={mockContextValue}>
<SwapAmountInput
label="To"
swappableTokens={mockSwappableTokens}
token={mockToken}
type="to"
/>
</SwapContext.Provider>,
);

const dropdown = screen.getByText('TokenSelectDropdown');
expect(dropdown).toBeInTheDocument();
});

it('renders a TokenChip component if swappableTokens are not passed as prop', () => {
render(
<SwapContext.Provider value={mockContextValue}>
<SwapAmountInput label="To" token={mockToken} type="to" />
</SwapContext.Provider>,
);

const dropdown = screen.getByText('TokenChip');
expect(dropdown).toBeInTheDocument();
});
});
46 changes: 41 additions & 5 deletions src/swap/components/SwapAmountInput.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { useCallback, useContext, useEffect, useMemo } from 'react';

import type { SwapAmountInputReact } from '../types';
import type { UseBalanceReturnType } from 'wagmi';
import { SwapContext } from '../context';
import { TextLabel1, TextLabel2 } from '../../internal/text';
import { TokenChip } from '../../token';
import { TokenChip, TokenSelectDropdown } from '../../token';
import { cn } from '../../utils/cn';
import { getRoundedAmount } from '../../utils/getRoundedAmount';
import { isValidAmount } from '../../utils/isValidAmount';
import { useBalance } from 'wagmi';
import type { SwapAmountInputReact } from '../types';
import type { UseBalanceReturnType } from 'wagmi';
import type { Token } from '../../token';

export function SwapAmountInput({ label, token, type }: SwapAmountInputReact) {
export function SwapAmountInput({
label,
swappableTokens,
token,
type,
}: SwapAmountInputReact) {
const {
address,
fromAmount,
fromToken,
setFromAmount,
setFromToken,
setToAmount,
setToToken,
toAmount,
toToken,
} = useContext(SwapContext);

const amount = useMemo(() => {
Expand All @@ -42,6 +50,13 @@ export function SwapAmountInput({ label, token, type }: SwapAmountInputReact) {
return setFromToken;
}, [type, setFromToken, setToToken]);

const selectedToken = useMemo(() => {
if (type === 'to') {
return toToken;
}
return fromToken;
}, [fromToken, toToken, type]);

const balanceResponse: UseBalanceReturnType = useBalance({
address,
...(token?.address && { token: token.address }),
Expand All @@ -53,6 +68,18 @@ export function SwapAmountInput({ label, token, type }: SwapAmountInputReact) {
}
}, [balanceResponse?.data, token]);

// we are mocking the token selectors so i'm not able
// to test this since the components aren't actually rendering
/* istanbul ignore next */
const filteredTokens = useMemo(() => {
if (type === 'to') {
return swappableTokens?.filter(
(t: Token) => t.symbol !== fromToken?.symbol,
);
}
return swappableTokens?.filter((t: Token) => t.symbol !== toToken?.symbol);
}, [fromToken, swappableTokens, toToken, type]);

const handleAmountChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
if (isValidAmount(event.target.value)) {
Expand Down Expand Up @@ -93,7 +120,16 @@ export function SwapAmountInput({ label, token, type }: SwapAmountInputReact) {
placeholder="0.0"
value={amount}
/>
<TokenChip token={token} />
{filteredTokens && (
<TokenSelectDropdown
options={filteredTokens}
setToken={setToken}
token={selectedToken}
/>
)}
{selectedToken && !filteredTokens && (
<TokenChip token={selectedToken} />
)}
</div>
<div className="mt-4 flex w-full justify-between">
<TextLabel2>~$0.0</TextLabel2>
Expand Down
1 change: 1 addition & 0 deletions src/swap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export type RawTransactionData = {
*/
export type SwapAmountInputReact = {
label: string; // Descriptive label for the input field
swappableTokens?: Token[];
token: Token; // Selected token
type: 'to' | 'from';
};
Expand Down

0 comments on commit 1b1dbe5

Please sign in to comment.