swap-ui/src/components/Swap.tsx

396 lines
9.6 KiB
TypeScript
Raw Normal View History

2021-05-13 00:28:32 -07:00
import { useState } from "react";
2021-05-12 13:10:52 -07:00
import { PublicKey } from "@solana/web3.js";
import { BN } from "@project-serum/anchor";
2021-05-12 13:10:52 -07:00
import {
makeStyles,
Card,
Button,
Typography,
TextField,
useTheme,
2021-05-12 13:10:52 -07:00
} from "@material-ui/core";
import { ExpandMore, ImportExportRounded } from "@material-ui/icons";
import { useSwapContext, useSwapFair } from "../context/Swap";
import {
useDexContext,
useOpenOrders,
2021-05-16 15:52:38 -07:00
useRouteVerbose,
useMarket,
FEE_MULTIPLIER,
} from "../context/Dex";
2021-05-17 12:01:35 -07:00
import { useTokenMap } from "../context/TokenList";
2021-05-17 17:21:25 -07:00
import { useMint, useOwnedTokenAccount } from "../context/Token";
2021-05-18 01:26:03 -07:00
import { useCanSwap, useReferral } from "../context/Swap";
2021-05-13 00:28:32 -07:00
import TokenDialog from "./TokenDialog";
2021-05-13 21:04:15 -07:00
import { SettingsButton } from "./Settings";
2021-05-14 11:30:21 -07:00
import { InfoLabel } from "./Info";
2021-05-12 13:10:52 -07:00
const useStyles = makeStyles((theme) => ({
2021-05-12 13:10:52 -07:00
card: {
width: theme.spacing(50),
borderRadius: theme.spacing(2),
boxShadow: "0px 0px 30px 5px rgba(0,0,0,0.075)",
padding: theme.spacing(2),
2021-05-12 13:10:52 -07:00
},
tab: {
width: "50%",
},
settingsButton: {
padding: 0,
},
swapButton: {
width: "100%",
borderRadius: theme.spacing(2),
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
fontSize: 16,
fontWeight: 700,
padding: theme.spacing(1.5),
2021-05-12 13:10:52 -07:00
},
swapToFromButton: {
display: "block",
margin: "10px auto 10px auto",
cursor: "pointer",
},
amountInput: {
fontSize: 22,
fontWeight: 600,
},
input: {
textAlign: "right",
},
swapTokenFormContainer: {
borderRadius: theme.spacing(2),
boxShadow: "0px 0px 15px 2px rgba(33,150,243,0.1)",
display: "flex",
justifyContent: "space-between",
padding: theme.spacing(1),
},
swapTokenSelectorContainer: {
marginLeft: theme.spacing(1),
display: "flex",
flexDirection: "column",
2021-05-26 15:33:53 -07:00
width: "50%",
},
balanceContainer: {
display: "flex",
alignItems: "center",
fontSize: "14px",
},
maxButton: {
marginLeft: theme.spacing(1),
color: theme.palette.primary.main,
fontWeight: 700,
fontSize: "12px",
cursor: "pointer",
},
tokenButton: {
display: "flex",
alignItems: "center",
cursor: "pointer",
marginBottom: theme.spacing(1),
2021-05-12 13:10:52 -07:00
},
}));
export default function SwapCard({
containerStyle,
contentStyle,
swapTokenContainerStyle,
}: {
containerStyle?: any;
contentStyle?: any;
swapTokenContainerStyle?: any;
}) {
2021-05-12 13:10:52 -07:00
const styles = useStyles();
return (
<Card className={styles.card} style={containerStyle}>
<SwapHeader />
<div style={contentStyle}>
<SwapFromForm style={swapTokenContainerStyle} />
<ArrowButton />
<SwapToForm style={swapTokenContainerStyle} />
<InfoLabel />
<SwapButton />
</div>
</Card>
2021-05-12 13:10:52 -07:00
);
}
export function SwapHeader() {
2021-05-12 13:10:52 -07:00
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "16px",
2021-05-12 13:10:52 -07:00
}}
>
<Typography
style={{
fontSize: 18,
fontWeight: 700,
2021-05-12 13:10:52 -07:00
}}
>
SWAP
2021-05-12 13:10:52 -07:00
</Typography>
<SettingsButton />
</div>
);
}
2021-05-14 11:30:21 -07:00
export function ArrowButton() {
2021-05-12 13:10:52 -07:00
const styles = useStyles();
const theme = useTheme();
2021-05-12 13:10:52 -07:00
const { swapToFromMints } = useSwapContext();
return (
<ImportExportRounded
className={styles.swapToFromButton}
fontSize="large"
htmlColor={theme.palette.primary.main}
onClick={swapToFromMints}
/>
2021-05-12 13:10:52 -07:00
);
}
function SwapFromForm({ style }: { style?: any }) {
2021-05-12 13:10:52 -07:00
const { fromMint, setFromMint, fromAmount, setFromAmount } = useSwapContext();
return (
<SwapTokenForm
from
style={style}
2021-05-12 13:10:52 -07:00
mint={fromMint}
setMint={setFromMint}
amount={fromAmount}
setAmount={setFromAmount}
/>
);
}
function SwapToForm({ style }: { style?: any }) {
2021-05-12 13:10:52 -07:00
const { toMint, setToMint, toAmount, setToAmount } = useSwapContext();
return (
<SwapTokenForm
from={false}
style={style}
2021-05-12 13:10:52 -07:00
mint={toMint}
setMint={setToMint}
amount={toAmount}
setAmount={setToAmount}
/>
);
}
export function SwapTokenForm({
from,
style,
2021-05-12 13:10:52 -07:00
mint,
setMint,
amount,
setAmount,
}: {
from: boolean;
style?: any;
2021-05-12 13:10:52 -07:00
mint: PublicKey;
setMint: (m: PublicKey) => void;
amount: number;
setAmount: (a: number) => void;
}) {
const styles = useStyles();
2021-05-13 00:28:32 -07:00
const [showTokenDialog, setShowTokenDialog] = useState(false);
2021-05-12 13:10:52 -07:00
const tokenAccount = useOwnedTokenAccount(mint);
2021-05-13 01:11:13 -07:00
const mintAccount = useMint(mint);
2021-05-12 13:10:52 -07:00
const balance =
tokenAccount &&
mintAccount &&
tokenAccount.account.amount.toNumber() / 10 ** mintAccount.decimals;
const formattedAmount =
mintAccount && amount
? amount.toLocaleString("fullwide", {
maximumFractionDigits: mintAccount.decimals,
useGrouping: false,
})
: amount;
2021-05-12 13:10:52 -07:00
return (
<div className={styles.swapTokenFormContainer} style={style}>
<div className={styles.swapTokenSelectorContainer}>
2021-05-13 00:28:32 -07:00
<TokenButton mint={mint} onClick={() => setShowTokenDialog(true)} />
<Typography color="textSecondary" className={styles.balanceContainer}>
2021-05-12 13:10:52 -07:00
{tokenAccount && mintAccount
? `Balance: ${balance?.toFixed(mintAccount.decimals)}`
2021-05-12 13:10:52 -07:00
: `-`}
{from && !!balance ? (
<span
className={styles.maxButton}
onClick={() => setAmount(balance)}
>
MAX
</span>
) : null}
2021-05-12 13:10:52 -07:00
</Typography>
</div>
<TextField
type="number"
value={formattedAmount}
onChange={(e) => setAmount(parseFloat(e.target.value))}
InputProps={{
disableUnderline: true,
classes: {
root: styles.amountInput,
input: styles.input,
},
}}
/>
2021-05-13 00:28:32 -07:00
<TokenDialog
setMint={setMint}
open={showTokenDialog}
onClose={() => setShowTokenDialog(false)}
/>
</div>
2021-05-12 13:10:52 -07:00
);
}
2021-05-13 00:28:32 -07:00
function TokenButton({
mint,
onClick,
}: {
mint: PublicKey;
onClick: () => void;
}) {
const styles = useStyles();
const theme = useTheme();
2021-05-12 13:10:52 -07:00
return (
<div onClick={onClick} className={styles.tokenButton}>
<TokenIcon mint={mint} style={{ width: theme.spacing(4) }} />
<TokenName mint={mint} style={{ fontSize: 14, fontWeight: 700 }} />
2021-05-12 13:10:52 -07:00
<ExpandMore />
</div>
2021-05-12 13:10:52 -07:00
);
}
2021-05-13 00:28:32 -07:00
export function TokenIcon({ mint, style }: { mint: PublicKey; style: any }) {
2021-05-15 16:16:28 -07:00
const tokenMap = useTokenMap();
let tokenInfo = tokenMap.get(mint.toString());
2021-05-12 13:10:52 -07:00
return (
<div
style={{
display: "flex",
justifyContent: "center",
flexDirection: "column",
}}
>
2021-05-15 16:16:28 -07:00
{tokenInfo?.logoURI ? (
2021-05-15 21:20:11 -07:00
<img alt="Logo" style={style} src={tokenInfo?.logoURI} />
2021-05-13 00:28:32 -07:00
) : (
<div style={style}></div>
)}
2021-05-12 13:10:52 -07:00
</div>
);
}
function TokenName({ mint, style }: { mint: PublicKey; style: any }) {
2021-05-15 16:16:28 -07:00
const tokenMap = useTokenMap();
const theme = useTheme();
2021-05-15 16:16:28 -07:00
let tokenInfo = tokenMap.get(mint.toString());
2021-05-12 13:10:52 -07:00
return (
<Typography
style={{
marginLeft: theme.spacing(2),
marginRight: theme.spacing(1),
...style,
}}
>
{tokenInfo?.symbol}
</Typography>
2021-05-12 13:10:52 -07:00
);
}
export function SwapButton() {
2021-05-12 13:10:52 -07:00
const styles = useStyles();
const {
fromMint,
toMint,
fromAmount,
slippage,
isClosingNewAccounts,
isStrict,
} = useSwapContext();
2021-05-15 00:39:56 -07:00
const { swapClient } = useDexContext();
const fromMintInfo = useMint(fromMint);
const toMintInfo = useMint(toMint);
const openOrders = useOpenOrders();
2021-05-16 15:52:38 -07:00
const route = useRouteVerbose(fromMint, toMint);
const fromMarket = useMarket(
route && route.markets ? route.markets[0] : undefined
);
const toMarket = useMarket(
route && route.markets ? route.markets[1] : undefined
);
2021-05-17 12:01:35 -07:00
const canSwap = useCanSwap();
2021-05-18 01:26:03 -07:00
const referral = useReferral(fromMarket);
const fair = useSwapFair();
2021-05-31 14:43:45 -07:00
const fromWallet = useOwnedTokenAccount(fromMint);
const toWallet = useOwnedTokenAccount(toMint);
const quoteMint = useMint(fromMarket && fromMarket.quoteMintAddress);
2021-05-12 13:10:52 -07:00
2021-05-17 12:01:35 -07:00
// Click handler.
2021-05-12 13:10:52 -07:00
const sendSwapTransaction = async () => {
2021-05-15 00:39:56 -07:00
if (!fromMintInfo || !toMintInfo) {
throw new Error("Unable to calculate mint decimals");
}
if (!fair) {
throw new Error("Invalid fair");
}
if (!quoteMint) {
throw new Error("Quote mint not found");
}
const amount = new BN(fromAmount * 10 ** fromMintInfo.decimals);
const minExchangeRate = {
rate: new BN((10 ** toMintInfo.decimals * FEE_MULTIPLIER) / fair)
.muln(100 - slippage)
.divn(100),
fromDecimals: fromMintInfo.decimals,
quoteDecimals: quoteMint.decimals,
strict: isStrict,
};
const fromOpenOrders = fromMarket
? openOrders.get(fromMarket?.address.toString())
: undefined;
const toOpenOrders = toMarket
? openOrders.get(toMarket?.address.toString())
: undefined;
2021-05-15 00:39:56 -07:00
await swapClient.swap({
fromMint,
toMint,
2021-05-31 14:43:45 -07:00
fromWallet: fromWallet ? fromWallet.publicKey : undefined,
toWallet: toWallet ? toWallet.publicKey : undefined,
2021-05-15 00:39:56 -07:00
amount,
minExchangeRate,
2021-05-18 01:26:03 -07:00
referral,
// Pass in the below parameters so that the client doesn't perform
// wasteful network requests when we already have the data.
fromMarket,
toMarket,
fromOpenOrders: fromOpenOrders ? fromOpenOrders[0].address : undefined,
toOpenOrders: toOpenOrders ? toOpenOrders[0].address : undefined,
2021-05-15 12:58:05 -07:00
// Auto close newly created open orders accounts.
close: isClosingNewAccounts,
2021-05-15 00:39:56 -07:00
});
2021-05-12 13:10:52 -07:00
};
return (
<Button
variant="contained"
className={styles.swapButton}
onClick={sendSwapTransaction}
2021-05-17 12:01:35 -07:00
disabled={!canSwap}
2021-05-12 13:10:52 -07:00
>
Swap
</Button>
);
}