diff --git a/src/context/Swap.tsx b/src/context/Swap.tsx index ed0c011..5dab0e6 100644 --- a/src/context/Swap.tsx +++ b/src/context/Swap.tsx @@ -1,250 +1,850 @@ -import * as assert from "assert"; -import React, { useContext, useState, useEffect } from "react"; -import { useAsync } from "react-async-hook"; -import { PublicKey } from "@solana/web3.js"; +import React, { useState } from "react"; import { + PublicKey, + Keypair, + Transaction, + SystemProgram, + Signer, + Account, + SYSVAR_RENT_PUBKEY, +} from "@solana/web3.js"; +import { + u64, Token, - ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, } from "@solana/spl-token"; -import { Market } from "@project-serum/serum"; -import { SRM_MINT, USDC_MINT, USDT_MINT } from "../utils/pubkeys"; +import { OpenOrders } from "@project-serum/serum"; +import { BN, Provider } from "@project-serum/anchor"; +import { + makeStyles, + Card, + Button, + Typography, + TextField, + useTheme, +} from "@material-ui/core"; +import { ExpandMore } from "@material-ui/icons"; +import { useSwapContext, useSwapFair } from "../context/Swap"; import { - useFairRoute, - useRouteVerbose, useDexContext, + useOpenOrders, + useRouteVerbose, + useMarket, FEE_MULTIPLIER, -} from "./Dex"; +} from "../context/Dex"; +import { useTokenMap } from "../context/TokenList"; import { - useTokenListContext, - SPL_REGISTRY_SOLLET_TAG, - SPL_REGISTRY_WORM_TAG, -} from "./TokenList"; -import { useOwnedTokenAccount } from "../context/Token"; + useMint, + useOwnedTokenAccount, + useTokenContext, +} from "../context/Token"; +import { useCanSwap, useReferral, useIsWrapSol } from "../context/Swap"; +import TokenDialog from "./TokenDialog"; +import { SettingsButton } from "./Settings"; +import { InfoLabel } from "./Info"; +import { SOL_MINT, WRAPPED_SOL_MINT, DEX_PID } from "../utils/pubkeys"; -const DEFAULT_SLIPPAGE_PERCENT = 0.5; - -export type SwapContext = { - // Mint being traded from. The user must own these tokens. - fromMint: PublicKey; - setFromMint: (m: PublicKey) => void; - - // Mint being traded to. The user will receive these tokens after the swap. - toMint: PublicKey; - setToMint: (m: PublicKey) => void; - - // Amount used for the swap. - fromAmount: number; - setFromAmount: (a: number) => void; - - // *Expected* amount received from the swap. - toAmount: number; - setToAmount: (a: number) => void; - - // Function to flip what we consider to be the "to" and "from" mints. - swapToFromMints: () => void; - - // The amount (in units of percent) a swap can be off from the estimate - // shown to the user. - slippage: number; - setSlippage: (n: number) => void; - - // Null if the user is using fairs directly from DEX prices. - // Otherwise, a user specified override for the price to use when calculating - // swap amounts. - fairOverride: number | null; - setFairOverride: (n: number | null) => void; - - // The referral *owner* address. Associated token accounts must be created, - // first, for this to be used. - referral?: PublicKey; - - // True if all newly created market accounts should be closed in the - // same user flow (ideally in the same transaction). - isClosingNewAccounts: boolean; - - // True if the swap exchange rate should be a function of nothing but the - // from and to tokens, ignoring any quote tokens that may have been - // accumulated by performing the swap. - // - // Always false (for now). - isStrict: boolean; - setIsStrict: (isStrict: boolean) => void; - - setIsClosingNewAccounts: (b: boolean) => void; -}; -const _SwapContext = React.createContext(null); - -export function SwapContextProvider(props: any) { - const [fromMint, setFromMint] = useState(props.fromMint ?? SRM_MINT); - const [toMint, setToMint] = useState(props.toMint ?? USDC_MINT); - const [fromAmount, _setFromAmount] = useState(props.fromAmount ?? 0); - const [toAmount, _setToAmount] = useState(props.toAmount ?? 0); - const [isClosingNewAccounts, setIsClosingNewAccounts] = useState(false); - const [isStrict, setIsStrict] = useState(false); - const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE_PERCENT); - const [fairOverride, setFairOverride] = useState(null); - const fair = _useSwapFair(fromMint, toMint, fairOverride); - const referral = props.referral; - - assert.ok(slippage >= 0); - - useEffect(() => { - if (!fair) { - return; - } - setFromAmount(fromAmount); - }, [fair]); - - const swapToFromMints = () => { - const oldFrom = fromMint; - const oldTo = toMint; - const oldToAmount = toAmount; - _setFromAmount(oldToAmount); - setFromMint(oldTo); - setToMint(oldFrom); - }; - - const setFromAmount = (amount: number) => { - if (fair === undefined) { - _setFromAmount(0); - _setToAmount(0); - return; - } - _setFromAmount(amount); - _setToAmount(FEE_MULTIPLIER * (amount / fair)); - }; - - const setToAmount = (amount: number) => { - if (fair === undefined) { - _setFromAmount(0); - _setToAmount(0); - return; - } - _setToAmount(amount); - _setFromAmount((amount * fair) / FEE_MULTIPLIER); - }; +const useStyles = makeStyles((theme) => ({ + card: { + width: 424, + borderRadius: 8, + boxShadow: "0px 0px 30px 5px rgba(0,0,0,0.075)", + padding: 0, + background: "#1C2222", + color: "#fff", + "& button:hover": { + opacity: 0.8, + }, + }, + tab: { + width: "50%", + }, + swapButton: { + width: "100%", + borderRadius: 8, + background: "linear-gradient(100.61deg, #B85900 0%, #FF810A 100%)", + color: "#fff", + fontSize: 20, + fontWeight: 500, + padding: theme.spacing(1.5), + "& .MuiButton-label": { + textTransform: "none", + }, + }, + swapToFromButton: { + display: "block", + margin: "10px auto 10px auto", + cursor: "pointer", + }, + amountInput: { + fontSize: 22, + fontWeight: 600, + color: "#fff", + }, + input: { + textAlign: "right", + }, + swapTokenFormContainer: { + borderRadius: theme.spacing(2), + display: "flex", + justifyContent: "space-between", + padding: theme.spacing(1), + }, + swapTokenSelectorContainer: { + marginLeft: 0, + display: "flex", + flexDirection: "column", + width: "50%", + }, + balanceContainer: { + display: "flex", + alignItems: "center", + fontSize: "14px", + }, + maxButton: { + marginLeft: theme.spacing(1), + color: "#F37B21", + zIndex: 1, + fontWeight: 700, + fontSize: "12px", + cursor: "pointer", + position: "absolute", + right: 15, + }, + tokenButton: { + display: "flex", + alignItems: "center", + cursor: "pointer", + marginBottom: theme.spacing(1), + }, +})); +export default function SwapCard({ + containerStyle, + contentStyle, + swapTokenContainerStyle, +}: { + containerStyle?: any; + contentStyle?: any; + swapTokenContainerStyle?: any; +}) { + const styles = useStyles(); + const { slippage } = useSwapContext(); return ( - <_SwapContext.Provider - value={{ - fromMint, - setFromMint, - toMint, - setToMint, - fromAmount, - setFromAmount, - toAmount, - setToAmount, - swapToFromMints, - slippage, - setSlippage, - fairOverride, - setFairOverride, - isClosingNewAccounts, - isStrict, - setIsStrict, - setIsClosingNewAccounts, - referral, + + +
+
+ + + + Slippage tolerance {slippage}% + +
+
+ + + + {/**/} + +
+
+
+ ); +} + +export function SwapHeader() { + const { slippage } = useSwapContext(); + return ( +
- {props.children} - + + Swap + + + +
); } -export function useSwapContext(): SwapContext { - const ctx = useContext(_SwapContext); - if (ctx === null) { - throw new Error("Context not available"); - } - return ctx; +export function ArrowButton() { + const styles = useStyles(); + const theme = useTheme(); + const { swapToFromMints } = useSwapContext(); + return ( + + ); + } + if (!isDexLoaded || !isTokensLoaded) { + return ( + + ); + } + return needsCreateAccounts ? ( + + ) : isWrapSol ? ( + + ) : isUnwrapSol ? ( + + ) : ( + + ); +} + +// If wrappedSolAccount is undefined, then creates the account with +// an associated token account. +async function wrapSol( + provider: Provider, + fromMint: PublicKey, + amount: BN, + wrappedSolAccount?: Keypair +): Promise<{ tx: Transaction; signers: Array }> { + const tx = new Transaction(); + const signers = wrappedSolAccount ? [wrappedSolAccount] : []; + let wrappedSolPubkey; + // Create new, rent exempt account. + if (wrappedSolAccount === undefined) { + wrappedSolPubkey = await Token.getAssociatedTokenAddress( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, - fromMarket.quoteMintAddress, - referral + fromMint, + provider.wallet.publicKey + ); + tx.add( + Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + fromMint, + wrappedSolPubkey, + provider.wallet.publicKey, + provider.wallet.publicKey + ) + ); + } else { + wrappedSolPubkey = wrappedSolAccount.publicKey; + tx.add( + SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: wrappedSolPubkey, + lamports: await Token.getMinBalanceRentForExemptAccount( + provider.connection + ), + space: 165, + programId: TOKEN_PROGRAM_ID, + }) ); - }, [fromMarket]); - - if (!asyncReferral.result) { - return undefined; } - return asyncReferral.result; + // Transfer lamports. These will be converted to an SPL balance by the + // token program. + if (fromMint.equals(SOL_MINT)) { + tx.add( + SystemProgram.transfer({ + fromPubkey: provider.wallet.publicKey, + toPubkey: wrappedSolPubkey, + lamports: amount.toNumber(), + }) + ); + } + // Initialize the account. + tx.add( + Token.createInitAccountInstruction( + TOKEN_PROGRAM_ID, + WRAPPED_SOL_MINT, + wrappedSolPubkey, + provider.wallet.publicKey + ) + ); + return { tx, signers }; +} + +function unwrapSol( + provider: Provider, + wrappedSol: PublicKey +): { tx: Transaction; signers: Array } { + const tx = new Transaction(); + tx.add( + Token.createCloseAccountInstruction( + TOKEN_PROGRAM_ID, + wrappedSol, + provider.wallet.publicKey, + provider.wallet.publicKey, + [] + ) + ); + return { tx, signers: [] }; }