import { useState } from "react" ; import { PublicKey , Keypair , Transaction , SystemProgram , Signer , } from "@solana/web3.js" ; import { Token , TOKEN_PROGRAM_ID } from "@solana/spl-token" ; import { BN , Provider } from "@project-serum/anchor" ; import { makeStyles , Card , Button , Typography , TextField , useTheme , } from "@material-ui/core" ; import { ExpandMore , ImportExportRounded } from "@material-ui/icons" ; import { useSwapContext , useSwapFair } from "../context/Swap" ; import { useDexContext , useOpenOrders , useRouteVerbose , useMarket , FEE_MULTIPLIER , } from "../context/Dex" ; import { useTokenMap } from "../context/TokenList" ; import { useMint , useOwnedTokenAccount } from "../context/Token" ; import { useCanSwap , useReferral } from "../context/Swap" ; import TokenDialog from "./TokenDialog" ; import { SettingsButton } from "./Settings" ; import { InfoLabel } from "./Info" ; import { SOL_MINT , WRAPPED_SOL_MINT } from "../utils/pubkeys" ; const useStyles = makeStyles ( ( theme ) => ( { card : { width : theme . spacing ( 50 ) , borderRadius : theme . spacing ( 2 ) , boxShadow : "0px 0px 30px 5px rgba(0,0,0,0.075)" , padding : theme . spacing ( 2 ) , } , 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 ) , } , 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" , 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 ) , } , } ) ) ; export default function SwapCard ( { containerStyle , contentStyle , swapTokenContainerStyle , } ) { 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>); } export function SwapHeader ( ) { return ( < div style = { { display : "flex" , justifyContent : "space-between" , marginBottom : "16px" , } } > < Typography style = { { fontSize : 18 , fontWeight : 700 , } } > SWAP < /Typography> < SettingsButton / > < /div>); } export function ArrowButton ( ) { const styles = useStyles ( ) ; const theme = useTheme ( ) ; const { swapToFromMints } = useSwapContext ( ) ; return ( < ImportExportRounded className = { styles . swapToFromButton } fontSize = "large" htmlColor = { theme . palette . primary . main } onClick = { swapToFromMints } / > ) ; } function SwapFromForm ( { style } ) { const { fromMint , setFromMint , fromAmount , setFromAmount } = useSwapContext ( ) ; return ( < SwapTokenForm from style = { style } mint = { fromMint } setMint = { setFromMint } amount = { fromAmount } setAmount = { setFromAmount } / > ) ; } function SwapToForm ( { style } ) { const { toMint , setToMint , toAmount , setToAmount } = useSwapContext ( ) ; return ( < SwapTokenForm from = { false } style = { style } mint = { toMint } setMint = { setToMint } amount = { toAmount } setAmount = { setToAmount } / > ) ; } export function SwapTokenForm ( { from , style , mint , setMint , amount , setAmount , } ) { const styles = useStyles ( ) ; const [ showTokenDialog , setShowTokenDialog ] = useState ( false ) ; const tokenAccount = useOwnedTokenAccount ( mint ) ; const mintAccount = useMint ( mint ) ; const balance = tokenAccount && mintAccount && tokenAccount . account . amount . toNumber ( ) / 10 ** mintAccount . decimals ; const formattedAmount = mintAccount && amount ? amount . toLocaleString ( "fullwide" , { maximumFractionDigits : mintAccount . decimals , useGrouping : false , } ) : amount ; return ( < div className = { styles . swapTokenFormContainer } style = { style } > < div className = { styles . swapTokenSelectorContainer } > < TokenButton mint = { mint } onClick = { ( ) => setShowTokenDialog ( true ) } /> < Typography color = "textSecondary" className = { styles . balanceContainer } > { tokenAccount && mintAccount ? `Balance: ${ balance ?. toFixed ( mintAccount . decimals ) }` : `-` } { from && ! ! balance ? ( < span className = { styles . maxButton } onClick = { ( ) => setAmount ( balance ) } > MAX < /span>) : null} < /Typography> < /div> < TextField type = "number" value = { formattedAmount } onChange = { ( e ) => setAmount ( parseFloat ( e . target . value ) ) } InputProps = { { disableUnderline : true , classes : { root : styles . amountInput , input : styles . input , } , } } /> < TokenDialog setMint = { setMint } open = { showTokenDialog } onClose = { ( ) => setShowTokenDialog ( false ) } /> < /div>); } function TokenButton ( { mint , onClick , } ) { const styles = useStyles ( ) ; const theme = useTheme ( ) ; return ( < div onClick = { onClick } className = { styles . tokenButton } > < TokenIcon mint = { mint } style = { { width : theme . spacing ( 4 ) } } /> < TokenName mint = { mint } style = { { fontSize : 14 , fontWeight : 700 } } /> < ExpandMore / > < /div>); } export function TokenIcon ( { mint , style } ) { const tokenMap = useTokenMap ( ) ; let tokenInfo = tokenMap . get ( mint . toString ( ) ) ; return ( < div style = { { display : "flex" , justifyContent : "center" , flexDirection : "column" , } } > { tokenInfo ?. logoURI ? ( < img alt = "Logo" style = { style } src = { tokenInfo ?. logoURI } / > ) : ( < div style = { style } > < / div > ) } < /div>); } function TokenName ( { mint , style } ) { const tokenMap = useTokenMap ( ) ; const theme = useTheme ( ) ; let tokenInfo = tokenMap . get ( mint . toString ( ) ) ; return ( < Typography style = { { marginLeft : theme . spacing ( 2 ) , marginRight : theme . spacing ( 1 ) , ... style , } } > { tokenInfo ?. symbol } < /Typography>); } export function SwapButton ( ) { const styles = useStyles ( ) ; const { fromMint , toMint , fromAmount , slippage , isClosingNewAccounts , isStrict , } = useSwapContext ( ) ; const { swapClient } = useDexContext ( ) ; const fromMintInfo = useMint ( fromMint ) ; const toMintInfo = useMint ( toMint ) ; const openOrders = useOpenOrders ( ) ; 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 ) ; const canSwap = useCanSwap ( ) ; const referral = useReferral ( fromMarket ) ; const fair = useSwapFair ( ) ; let fromWallet = useOwnedTokenAccount ( fromMint ) ; let toWallet = useOwnedTokenAccount ( toMint ) ; const quoteMint = fromMarket && fromMarket . quoteMintAddress ; const quoteMintInfo = useMint ( quoteMint ) ; const quoteWallet = useOwnedTokenAccount ( quoteMint ) ; const sendSwapTransaction = async ( ) => { if ( ! fromMintInfo || ! toMintInfo ) { throw new Error ( "Unable to calculate mint decimals" ) ; } if ( ! fair ) { throw new Error ( "Invalid fair" ) ; } if ( ! quoteMint || ! quoteMintInfo ) { throw new Error ( "Quote mint not found" ) ; } const amount = new BN ( fromAmount * 10 ** fromMintInfo . decimals ) ; const isSol = fromMint . equals ( SOL_MINT ) || toMint . equals ( SOL_MINT ) ; const wrappedSolAccount = isSol ? Keypair . generate ( ) : undefined ; let txs = await ( async ( ) => { if ( ! fromMarket ) { throw new Error ( "Market undefined" ) ; } const minExchangeRate = { rate : new BN ( ( 10 ** toMintInfo . decimals * FEE_MULTIPLIER ) / fair ) . muln ( 100 - slippage ) . divn ( 100 ) , fromDecimals : fromMintInfo . decimals , quoteDecimals : quoteMintInfo . decimals , strict : isStrict , } ; const fromOpenOrders = fromMarket ? openOrders . get ( fromMarket ?. address . toString ( ) ) : undefined ; const toOpenOrders = toMarket ? openOrders . get ( toMarket ?. address . toString ( ) ) : undefined ; const fromWalletAddr = fromMint . equals ( SOL_MINT ) ? wrappedSolAccount ! . publicKey : fromWallet ? fromWallet . publicKey : undefined ; const toWalletAddr = toMint . equals ( SOL_MINT ) ? wrappedSolAccount ! . publicKey : toWallet ? toWallet . publicKey : undefined ; return await swapClient . swapTxs ( { fromMint , toMint , quoteMint , amount , minExchangeRate , referral , fromMarket , toMarket , fromOpenOrders : fromOpenOrders ? fromOpenOrders [ 0 ] . address : undefined , toOpenOrders : toOpenOrders ? toOpenOrders [ 0 ] . address : undefined , fromWallet : fromWalletAddr , toWallet : toWalletAddr , quoteWallet : quoteWallet ? quoteWallet . publicKey : undefined , close : isClosingNewAccounts , } ) ; } ) ( ) ; if ( isSol ) { if ( txs . length > 1 ) { throw new Error ( "SOL must be swapped in a single transaction" ) ; } const { tx : wrapTx , signers : wrapSigners } = await wrapSol ( swapClient . program . provider , wrappedSolAccount as Keypair , fromMint , amount ) ; const { tx : unwrapTx , signers : unwrapSigners } = unwrapSol ( swapClient . program . provider , wrappedSolAccount as Keypair ) ; const tx = new Transaction ( ) ; tx . add ( wrapTx ) ; tx . add ( txs [ 0 ] . tx ) ; tx . add ( unwrapTx ) ; txs [ 0 ] . tx = tx ; txs [ 0 ] . signers . push ( ... wrapSigners ) ; txs [ 0 ] . signers . push ( ... unwrapSigners ) ; } await swapClient . program . provider . sendAll ( txs ) ; } ; return ( < Button variant = "contained" className = { styles . swapButton } onClick = { sendSwapTransaction } disabled = { ! canSwap } > Swap < /Button>); } async function wrapSol ( provider , wrappedSolAccount , fromMint , amount ) { const tx = new Transaction ( ) ; const signers = [ wrappedSolAccount ] ; tx . add ( SystemProgram . createAccount ( { fromPubkey : provider . wallet . publicKey , newAccountPubkey : wrappedSolAccount . publicKey , lamports : await Token . getMinBalanceRentForExemptAccount ( provider . connection ) , space : 165 , programId : TOKEN_PROGRAM_ID , } ) ) ; if ( fromMint . equals ( SOL_MINT ) ) { tx . add ( SystemProgram . transfer ( { fromPubkey : provider . wallet . publicKey , toPubkey : wrappedSolAccount . publicKey , lamports : amount . toNumber ( ) , } ) ) ; } tx . add ( Token . createInitAccountInstruction ( TOKEN_PROGRAM_ID , WRAPPED_SOL_MINT , wrappedSolAccount . publicKey , provider . wallet . publicKey ) ) ; return { tx , signers } ; } function unwrapSol ( provider , wrappedSolAccount ) { const tx = new Transaction ( ) ; tx . add ( Token . createCloseAccountInstruction ( TOKEN_PROGRAM_ID , wrappedSolAccount . publicKey , provider . wallet . publicKey , provider . wallet . publicKey , [ ] ) ) ; return { tx , signers : [ ] } ; }