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";
|
2021-05-17 11:33:35 -07:00
|
|
|
import { BN } from "@project-serum/anchor";
|
2021-05-12 13:10:52 -07:00
|
|
|
import {
|
|
|
|
makeStyles,
|
|
|
|
Card,
|
|
|
|
Button,
|
|
|
|
Typography,
|
|
|
|
TextField,
|
2021-05-26 09:47:53 -07:00
|
|
|
useTheme,
|
2021-05-12 13:10:52 -07:00
|
|
|
} from "@material-ui/core";
|
2021-05-26 09:47:53 -07:00
|
|
|
import { ExpandMore, ImportExportRounded } from "@material-ui/icons";
|
2021-05-19 18:59:29 -07:00
|
|
|
import { useSwapContext, useSwapFair } from "../context/Swap";
|
2021-05-15 12:27:13 -07:00
|
|
|
import {
|
|
|
|
useDexContext,
|
|
|
|
useOpenOrders,
|
2021-05-16 15:52:38 -07:00
|
|
|
useRouteVerbose,
|
2021-05-15 12:27:13 -07:00
|
|
|
useMarket,
|
2021-06-15 10:49:17 -07:00
|
|
|
FEE_MULTIPLIER,
|
2021-05-17 11:33:35 -07:00
|
|
|
} 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
|
|
|
|
2021-05-26 09:47:53 -07:00
|
|
|
const useStyles = makeStyles((theme) => ({
|
2021-05-12 13:10:52 -07:00
|
|
|
card: {
|
2021-06-15 12:02:50 -07:00
|
|
|
width: theme.spacing(50),
|
|
|
|
borderRadius: theme.spacing(2),
|
2021-05-26 09:47:53 -07:00
|
|
|
boxShadow: "0px 0px 30px 5px rgba(0,0,0,0.075)",
|
2021-06-15 12:02:50 -07:00
|
|
|
padding: theme.spacing(2),
|
2021-05-12 13:10:52 -07:00
|
|
|
},
|
|
|
|
tab: {
|
|
|
|
width: "50%",
|
|
|
|
},
|
|
|
|
settingsButton: {
|
|
|
|
padding: 0,
|
|
|
|
},
|
|
|
|
swapButton: {
|
|
|
|
width: "100%",
|
2021-06-15 12:02:50 -07:00
|
|
|
borderRadius: theme.spacing(2),
|
2021-05-26 09:47:53 -07:00
|
|
|
backgroundColor: theme.palette.primary.main,
|
|
|
|
color: theme.palette.primary.contrastText,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: 700,
|
2021-06-15 12:02:50 -07:00
|
|
|
padding: theme.spacing(1.5),
|
2021-05-12 13:10:52 -07:00
|
|
|
},
|
|
|
|
swapToFromButton: {
|
|
|
|
display: "block",
|
2021-05-26 09:47:53 -07:00
|
|
|
margin: "10px auto 10px auto",
|
|
|
|
cursor: "pointer",
|
|
|
|
},
|
|
|
|
amountInput: {
|
|
|
|
fontSize: 22,
|
|
|
|
fontWeight: 600,
|
|
|
|
},
|
|
|
|
input: {
|
|
|
|
textAlign: "right",
|
|
|
|
},
|
|
|
|
swapTokenFormContainer: {
|
2021-06-15 12:02:50 -07:00
|
|
|
borderRadius: theme.spacing(2),
|
2021-05-26 09:47:53 -07:00
|
|
|
boxShadow: "0px 0px 15px 2px rgba(33,150,243,0.1)",
|
|
|
|
display: "flex",
|
|
|
|
justifyContent: "space-between",
|
2021-06-15 12:02:50 -07:00
|
|
|
padding: theme.spacing(1),
|
2021-05-26 09:47:53 -07:00
|
|
|
},
|
|
|
|
swapTokenSelectorContainer: {
|
2021-06-15 12:02:50 -07:00
|
|
|
marginLeft: theme.spacing(1),
|
2021-05-26 09:47:53 -07:00
|
|
|
display: "flex",
|
|
|
|
flexDirection: "column",
|
2021-05-26 15:33:53 -07:00
|
|
|
width: "50%",
|
2021-05-26 09:47:53 -07:00
|
|
|
},
|
|
|
|
balanceContainer: {
|
|
|
|
display: "flex",
|
|
|
|
alignItems: "center",
|
|
|
|
fontSize: "14px",
|
|
|
|
},
|
|
|
|
maxButton: {
|
2021-06-15 12:02:50 -07:00
|
|
|
marginLeft: theme.spacing(1),
|
2021-05-26 09:47:53 -07:00
|
|
|
color: theme.palette.primary.main,
|
|
|
|
fontWeight: 700,
|
|
|
|
fontSize: "12px",
|
|
|
|
cursor: "pointer",
|
|
|
|
},
|
|
|
|
tokenButton: {
|
|
|
|
display: "flex",
|
|
|
|
alignItems: "center",
|
|
|
|
cursor: "pointer",
|
2021-06-15 12:02:50 -07:00
|
|
|
marginBottom: theme.spacing(1),
|
2021-05-12 13:10:52 -07:00
|
|
|
},
|
|
|
|
}));
|
|
|
|
|
2021-05-26 09:47:53 -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 (
|
2021-05-26 09:47:53 -07:00
|
|
|
<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
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-06-13 10:32:34 -07:00
|
|
|
export function SwapHeader() {
|
2021-05-12 13:10:52 -07:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
style={{
|
|
|
|
display: "flex",
|
|
|
|
justifyContent: "space-between",
|
2021-06-15 12:02:50 -07:00
|
|
|
marginBottom: "16px",
|
2021-05-12 13:10:52 -07:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Typography
|
|
|
|
style={{
|
2021-05-26 09:47:53 -07:00
|
|
|
fontSize: 18,
|
|
|
|
fontWeight: 700,
|
2021-05-12 13:10:52 -07:00
|
|
|
}}
|
|
|
|
>
|
2021-05-26 09:47:53 -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();
|
2021-05-26 09:47:53 -07:00
|
|
|
const theme = useTheme();
|
2021-05-12 13:10:52 -07:00
|
|
|
const { swapToFromMints } = useSwapContext();
|
|
|
|
return (
|
2021-05-26 09:47:53 -07:00
|
|
|
<ImportExportRounded
|
|
|
|
className={styles.swapToFromButton}
|
|
|
|
fontSize="large"
|
|
|
|
htmlColor={theme.palette.primary.main}
|
|
|
|
onClick={swapToFromMints}
|
|
|
|
/>
|
2021-05-12 13:10:52 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-05-26 09:47:53 -07:00
|
|
|
function SwapFromForm({ style }: { style?: any }) {
|
2021-05-12 13:10:52 -07:00
|
|
|
const { fromMint, setFromMint, fromAmount, setFromAmount } = useSwapContext();
|
|
|
|
return (
|
|
|
|
<SwapTokenForm
|
2021-05-26 09:47:53 -07:00
|
|
|
from
|
|
|
|
style={style}
|
2021-05-12 13:10:52 -07:00
|
|
|
mint={fromMint}
|
|
|
|
setMint={setFromMint}
|
|
|
|
amount={fromAmount}
|
|
|
|
setAmount={setFromAmount}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-05-26 09:47:53 -07:00
|
|
|
function SwapToForm({ style }: { style?: any }) {
|
2021-05-12 13:10:52 -07:00
|
|
|
const { toMint, setToMint, toAmount, setToAmount } = useSwapContext();
|
|
|
|
return (
|
|
|
|
<SwapTokenForm
|
2021-05-26 09:47:53 -07:00
|
|
|
from={false}
|
|
|
|
style={style}
|
2021-05-12 13:10:52 -07:00
|
|
|
mint={toMint}
|
|
|
|
setMint={setToMint}
|
|
|
|
amount={toAmount}
|
|
|
|
setAmount={setToAmount}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-06-13 10:32:34 -07:00
|
|
|
export function SwapTokenForm({
|
2021-05-26 09:47:53 -07:00
|
|
|
from,
|
|
|
|
style,
|
2021-05-12 13:10:52 -07:00
|
|
|
mint,
|
|
|
|
setMint,
|
|
|
|
amount,
|
|
|
|
setAmount,
|
|
|
|
}: {
|
2021-05-26 09:47:53 -07:00
|
|
|
from: boolean;
|
|
|
|
style?: any;
|
2021-05-12 13:10:52 -07:00
|
|
|
mint: PublicKey;
|
|
|
|
setMint: (m: PublicKey) => void;
|
|
|
|
amount: number;
|
|
|
|
setAmount: (a: number) => void;
|
|
|
|
}) {
|
2021-05-26 09:47:53 -07:00
|
|
|
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
|
|
|
|
2021-05-26 09:47:53 -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 (
|
2021-05-26 09:47:53 -07:00
|
|
|
<div className={styles.swapTokenFormContainer} style={style}>
|
|
|
|
<div className={styles.swapTokenSelectorContainer}>
|
2021-05-13 00:28:32 -07:00
|
|
|
<TokenButton mint={mint} onClick={() => setShowTokenDialog(true)} />
|
2021-05-26 09:47:53 -07:00
|
|
|
<Typography color="textSecondary" className={styles.balanceContainer}>
|
2021-05-12 13:10:52 -07:00
|
|
|
{tokenAccount && mintAccount
|
2021-05-26 09:47:53 -07:00
|
|
|
? `Balance: ${balance?.toFixed(mintAccount.decimals)}`
|
2021-05-12 13:10:52 -07:00
|
|
|
: `-`}
|
2021-05-26 09:47:53 -07:00
|
|
|
{from && !!balance ? (
|
|
|
|
<span
|
|
|
|
className={styles.maxButton}
|
|
|
|
onClick={() => setAmount(balance)}
|
|
|
|
>
|
|
|
|
MAX
|
|
|
|
</span>
|
|
|
|
) : null}
|
2021-05-12 13:10:52 -07:00
|
|
|
</Typography>
|
|
|
|
</div>
|
2021-05-26 09:47:53 -07:00
|
|
|
<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)}
|
|
|
|
/>
|
2021-05-26 09:47:53 -07:00
|
|
|
</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;
|
|
|
|
}) {
|
2021-05-26 09:47:53 -07:00
|
|
|
const styles = useStyles();
|
2021-06-15 12:02:50 -07:00
|
|
|
const theme = useTheme();
|
2021-05-26 09:47:53 -07:00
|
|
|
|
2021-05-12 13:10:52 -07:00
|
|
|
return (
|
2021-05-26 09:47:53 -07:00
|
|
|
<div onClick={onClick} className={styles.tokenButton}>
|
2021-06-15 12:02:50 -07:00
|
|
|
<TokenIcon mint={mint} style={{ width: theme.spacing(4) }} />
|
2021-05-26 09:47:53 -07:00
|
|
|
<TokenName mint={mint} style={{ fontSize: 14, fontWeight: 700 }} />
|
2021-05-12 13:10:52 -07:00
|
|
|
<ExpandMore />
|
2021-05-26 09:47:53 -07:00
|
|
|
</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>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-05-26 09:47:53 -07:00
|
|
|
function TokenName({ mint, style }: { mint: PublicKey; style: any }) {
|
2021-05-15 16:16:28 -07:00
|
|
|
const tokenMap = useTokenMap();
|
2021-06-15 12:02:50 -07:00
|
|
|
const theme = useTheme();
|
2021-05-15 16:16:28 -07:00
|
|
|
let tokenInfo = tokenMap.get(mint.toString());
|
2021-06-15 12:02:50 -07:00
|
|
|
|
2021-05-12 13:10:52 -07:00
|
|
|
return (
|
2021-06-15 12:02:50 -07:00
|
|
|
<Typography
|
|
|
|
style={{
|
|
|
|
marginLeft: theme.spacing(2),
|
|
|
|
marginRight: theme.spacing(1),
|
|
|
|
...style,
|
|
|
|
}}
|
|
|
|
>
|
2021-05-26 09:47:53 -07:00
|
|
|
{tokenInfo?.symbol}
|
|
|
|
</Typography>
|
2021-05-12 13:10:52 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-06-13 10:32:34 -07:00
|
|
|
export function SwapButton() {
|
2021-05-12 13:10:52 -07:00
|
|
|
const styles = useStyles();
|
2021-06-04 12:41:20 -07:00
|
|
|
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);
|
2021-05-15 12:27:13 -07:00
|
|
|
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);
|
2021-05-19 18:59:29 -07:00
|
|
|
const fair = useSwapFair();
|
2021-05-31 14:43:45 -07:00
|
|
|
const fromWallet = useOwnedTokenAccount(fromMint);
|
|
|
|
const toWallet = useOwnedTokenAccount(toMint);
|
2021-06-04 12:41:20 -07:00
|
|
|
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");
|
|
|
|
}
|
2021-05-19 18:59:29 -07:00
|
|
|
if (!fair) {
|
|
|
|
throw new Error("Invalid fair");
|
|
|
|
}
|
2021-06-04 12:41:20 -07:00
|
|
|
if (!quoteMint) {
|
|
|
|
throw new Error("Quote mint not found");
|
|
|
|
}
|
|
|
|
const amount = new BN(fromAmount * 10 ** fromMintInfo.decimals);
|
2021-05-19 18:59:29 -07:00
|
|
|
const minExchangeRate = {
|
2021-06-15 10:49:17 -07:00
|
|
|
rate: new BN((10 ** toMintInfo.decimals * FEE_MULTIPLIER) / fair)
|
2021-05-19 18:59:29 -07:00
|
|
|
.muln(100 - slippage)
|
|
|
|
.divn(100),
|
2021-06-04 12:41:20 -07:00
|
|
|
fromDecimals: fromMintInfo.decimals,
|
|
|
|
quoteDecimals: quoteMint.decimals,
|
|
|
|
strict: isStrict,
|
2021-05-19 18:59:29 -07:00
|
|
|
};
|
2021-05-15 12:27:13 -07:00
|
|
|
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,
|
2021-05-19 18:59:29 -07:00
|
|
|
minExchangeRate,
|
2021-05-18 01:26:03 -07:00
|
|
|
referral,
|
2021-05-15 12:27:13 -07:00
|
|
|
// 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>
|
|
|
|
);
|
|
|
|
}
|