Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Swap component refactor #522

Merged
merged 31 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d32e47c
add swap component
abcrane123 Jun 12, 2024
2113b5f
update swap component types
abcrane123 Jun 12, 2024
eb7776e
update docs
abcrane123 Jun 11, 2024
c2fae82
add onsubmit
abcrane123 Jun 11, 2024
5cd8a44
refactor swap component
abcrane123 Jun 12, 2024
fbafe15
continue refactor
abcrane123 Jun 12, 2024
cb4a4f8
update type
abcrane123 Jun 12, 2024
e92d617
remove unused code
abcrane123 Jun 12, 2024
a284173
fix type
abcrane123 Jun 12, 2024
fd80163
add comment and fix type
abcrane123 Jun 12, 2024
76fd5b6
temp commit
abcrane123 Jun 12, 2024
e2f94c1
simplify and refactor
abcrane123 Jun 12, 2024
7b4ee10
update docs
abcrane123 Jun 12, 2024
a1d60e6
address pr comments
abcrane123 Jun 12, 2024
1906bd5
cleanup types
abcrane123 Jun 12, 2024
8b4ce11
expand funtionality and add onsuccess / onerror
abcrane123 Jun 12, 2024
0f76402
update imports
abcrane123 Jun 12, 2024
15aa579
address pr comments
abcrane123 Jun 12, 2024
54ffb47
move func to utils
abcrane123 Jun 12, 2024
dc7899c
enable quote for from and to token
abcrane123 Jun 12, 2024
e8378d9
move handleSubmit to SwapButton
abcrane123 Jun 12, 2024
9e0a929
refactor swap quote into hook
abcrane123 Jun 12, 2024
34fc7f4
remove error handling
abcrane123 Jun 12, 2024
0823347
remove useCallback and update types
abcrane123 Jun 12, 2024
4eebdfb
cleanup hook
abcrane123 Jun 12, 2024
0c0c4b3
refactor swap quote
abcrane123 Jun 13, 2024
0b6e86b
reorder types
abcrane123 Jun 13, 2024
8057359
rename SwapAmountInput and remove old version
abcrane123 Jun 13, 2024
2adc86c
add tests
abcrane123 Jun 13, 2024
16ae400
update docs
abcrane123 Jun 13, 2024
79ba44b
remove imports
abcrane123 Jun 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions site/docs/pages/swap/swap.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Swap, SwapAmountInputV2, SwapButton } from '../../../../src/swap';
import App from '../App';

# `<Swap />`

The `Swap` component is a comprehensive interface for users to execute token swaps. It includes two instances of the `SwapAmountInputV2` component, enabling users to specify the amount of tokens to sell and buy. Additionally, the component features a "Swap" button for initiating the transaction.

## Usage

```tsx [code]
<Swap account={account} onError={onError} onSuccess={onSuccess}>
<SwapAmountInputV2 label="Sell" token={fromToken} type="from" />
<SwapAmountInputV2 label="Buy" token={toToken} type="to" />
<SwapButton />
</Swap>
```

<App>
<Swap>
<SwapAmountInputV2
label="Sell"
token={{
name: 'Ethereum',
address: '',
symbol: 'ETH',
decimals: 18,
image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
chainId: 8453,
}}
type="from"
/>
<SwapAmountInputV2
label="Buy"
token={{
name: 'USDC',
address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
symbol: 'USDC',
decimals: 6,
image:
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2',
chainId: 8453,
}}
type="to"
/>
<SwapButton />
</Swap>
</App>

## Props

[`SwapReact`](/swap/types#SwapReact)
18 changes: 12 additions & 6 deletions site/docs/pages/swap/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ description: Glossary of Types in Swap components & utilities.

```ts
type SwapAmountInputReact = {
amount?: string; // Token amount
disabled?: boolean; // Whether the input is disabled
label: string; // Descriptive label for the input field
setAmount: (amount: string) => void; // Callback function when the amount changes
setToken: () => void; // Callback function when the token selector is clicked
swappableTokens: Token[]; // Tokens available for swap
token?: Token; // Selected token
tokenBalance?: string; // Amount of selected token user owns
type: 'to' | 'from'; // Identifies if component is for toToken or fromToken
};
```

## `SwapReact`

```ts
export type SwapReact = {
account: Account; // Ethereum account
children: ReactNode; // Children components to render
onError?: (error: SwapError) => void; // Callback when swap is unsuccessful
onSuccess?: (swapTransaction: BuildSwapTransaction) => void; // Callback when swap is successful
};
```
4 changes: 4 additions & 0 deletions site/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export const sidebar = [
{
text: 'Components',
items: [
{
text: 'Swap',
link: '/swap/swap',
},
{
text: 'SwapAmountInput',
link: '/swap/swap-amount-input',
Expand Down
110 changes: 110 additions & 0 deletions src/swap/components/Swap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { buildSwapTransaction } from '../core/buildSwapTransaction';
import { getSwapQuote } from '../core/getSwapQuote';
import { cn } from '../../lib/utils';
import { SwapContext } from '../context';
import type { SwapError, SwapReact } from '../types';
import type { Token } from '../../token';

function isSwapError(response: unknown): response is SwapError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move utils in their own file.

return response !== null && typeof response === 'object' && 'error' in response;
}

export function Swap({ account, children, onError, onSuccess }: SwapReact) {
const [fromAmount, setFromAmount] = useState('');
const [fromToken, setFromToken] = useState<Token>();
const [toAmount, setToAmount] = useState('');
const [toToken, setToToken] = useState<Token>();
const [swapQuoteError, setSwapQuoteError] = useState('');
const [swapTransactionError, setSwapTransactionError] = useState('');

const handleSubmit = useCallback(async () => {
setSwapQuoteError('');
setSwapTransactionError('');
if (account && fromToken && toToken && fromAmount) {
try {
const response = await buildSwapTransaction({
amount: fromAmount,
fromAddress: account.address,
from: fromToken,
to: toToken,
});
if (isSwapError(response)) {
setSwapTransactionError(response.error);
onError?.(response);
} else {
onSuccess?.(response);
}
} catch (error) {
onError?.(error as SwapError);
}
}
}, [account, fromAmount, fromToken, toToken]);

const handleGetSwapQuote = useCallback(async () => {
setSwapQuoteError('');
setSwapTransactionError('');
if (fromToken && toToken && fromAmount) {
try {
const response = await getSwapQuote({ from: fromToken, to: toToken, amount: fromAmount });
if (isSwapError(response)) {
setSwapQuoteError(response.error);
} else {
setToAmount(response?.toAmount);
}
} catch (error) {
onError?.(error as SwapError);
}
}
}, [fromAmount, fromToken, toToken]);

useEffect(() => {
if (fromToken && toToken && fromAmount) {
handleGetSwapQuote();
}
}, [fromAmount, fromToken, toToken]);

const value = useMemo(() => {
return {
fromAmount,
fromToken,
onSubmit: handleSubmit,
setFromAmount,
setFromToken,
setToAmount,
setToToken,
toAmount,
toToken,
};
}, [
fromAmount,
fromToken,
handleSubmit,
setFromAmount,
setToAmount,
setToToken,
toAmount,
toToken,
]);

return (
<SwapContext.Provider value={value}>
<div className="flex w-[400px] flex-col rounded-xl bg-white">
<label
className={cn(
'box-border w-full border-b border-solid p-4 text-base',
'font-semibold leading-6 text-[#030712]',
)}
>
Swap
</label>
{(swapQuoteError || swapTransactionError) && (
<p className={cn('p-4 text-center text-[14px] text-[red]')}>
{swapQuoteError || swapTransactionError}
</p>
)}
{children}
</div>
</SwapContext.Provider>
);
}
4 changes: 2 additions & 2 deletions src/swap/components/SwapAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ export function SwapAmountInput({
const handleAmountChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
if (isValidAmount(event.target.value)) {
setAmount(event.target.value);
setAmount?.(event.target.value);
}
},
[setAmount],
);

const handleMaxButtonClick = useCallback(() => {
if (tokenBalance && isValidAmount(tokenBalance)) {
setAmount(tokenBalance);
setAmount?.(tokenBalance);
}
}, [tokenBalance, setAmount]);

Expand Down
69 changes: 69 additions & 0 deletions src/swap/components/SwapAmountInputV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useCallback, useContext, useEffect, useMemo } from 'react';

import { isValidAmount } from '../utils';
import { TokenChip } from '../../token';
import { cn } from '../../lib/utils';
import { SwapContext } from '../context';
import type { SwapAmountInputReact } from '../types';

export function SwapAmountInputV2({ label, token, type }: SwapAmountInputReact) {
const { fromAmount, setFromAmount, setFromToken, setToAmount, setToToken, toAmount } =
useContext(SwapContext);

const amount = useMemo(() => {
if (type === 'to') {
return toAmount;
}
return fromAmount;
}, [type, toAmount, fromAmount]);

const setAmount = useMemo(() => {
if (type === 'to') {
return setToAmount;
}
return setFromAmount;
}, [type, setToAmount, setFromAmount]);

const setToken = useMemo(() => {
if (type === 'to') {
return setToToken;
}
return setFromToken;
}, [type, setFromToken, setToToken]);

useEffect(() => {
if (token) {
setToken(token);
}
}, [token]);

const handleAmountChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
if (isValidAmount(event.target.value)) {
setAmount?.(event.target.value);
}
}, []);

return (
<div
className={cn(
'box-border flex w-full flex-col items-start',
'gap-[11px] border-b border-solid bg-[#FFF] p-4',
)}
data-testid="ockSwapAmountInput_Container"
>
<div className="flex w-full items-center justify-between">
<label className="text-sm font-semibold text-[#030712]">{label}</label>
</div>
<div className="flex w-full items-center justify-between">
<TokenChip token={token} />
</div>
<input
className="w-full border-[none] bg-transparent text-5xl text-[black]"
data-testid="ockSwapAmountInput_Input"
onChange={handleAmountChange}
placeholder="0"
value={amount}
/>
</div>
);
}
26 changes: 26 additions & 0 deletions src/swap/components/SwapButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useCallback, useContext } from 'react';
import { cn } from '../../lib/utils';
import { SwapContext } from '../context';

export function SwapButton() {
const { onSubmit, fromAmount, fromToken, toAmount, toToken } = useContext(SwapContext);

const handleSubmit = useCallback(() => {
onSubmit();
}, [onSubmit]);

return (
<div className="w-full p-4">
<button
className={cn(
'w-full rounded-[100px] bg-blue-700',
'px-4 py-3 text-base font-medium leading-6 text-white',
)}
onClick={handleSubmit}
disabled={!fromAmount || !fromToken || !toAmount || !toToken}
>
Swap
</button>
</div>
);
}
10 changes: 10 additions & 0 deletions src/swap/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createContext } from 'react';
import { SwapContextType } from './types';

export const SwapContext = createContext<SwapContextType>({
fromAmount: '',
setFromAmount: () => {},
toAmount: '',
setToAmount: () => {},
account: undefined,
});
5 changes: 4 additions & 1 deletion src/swap/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// 🌲☀️🌲
export { getSwapQuote } from './core/getSwapQuote';
export { buildSwapTransaction } from './core/buildSwapTransaction';
export { Swap } from './components/Swap'
export { SwapAmountInput } from './components/SwapAmountInput';
export { SwapAmountInputV2 } from './components/SwapAmountInputV2';
export { SwapButton } from './components/SwapButton';
export type {
BuildSwapTransactionParams,
BuildSwapTransactionResponse,
Expand Down
Loading
Loading