import React, { FunctionComponent, useEffect, useMemo, useState, useRef, } from "react"; import { PublicKey, Transaction } from "@solana/web3.js"; import { useConnection, useWallet } from "@solana/wallet-adapter-react"; import { ButtonBorderGradient } from "../Buttons"; import { TokenInfo } from "@solana/spl-token-registry"; import { SelectCoin } from "./SelectCoin"; import { RefreshIcon, SwitchVerticalIcon } from "@heroicons/react/solid"; import { FEES_BPS } from "../../settings/fees"; import { WalletMultiButton, WalletModalProvider, } from "@solana/wallet-adapter-react-ui"; import { useLocalStorageState, useInterval } from "ahooks"; import { Slippage } from "./Slippage"; import { InlineResponseDefaultMarketInfos, InlineResponseDefaultData, } from "@jup-ag/api"; import { SwapRoute } from "../SwapRoute"; import { toast } from "react-toastify"; import Loading from "../Loading"; import emoji from "../../assets/no-route.png"; import { getFeeAddress } from "../../utils/fees"; import { RenderUpdate } from "../../utils/notifications"; import { nanoid } from "nanoid"; import { Balance } from "./Balance"; import { retry, sendRawTransaction, STR_MINT } from "@stream-swap/ui"; import { GENESYS_GO_CONNECTIONS } from "../../utils/connection"; import { useTokenAccounts } from "@stream-swap/ui"; import round from "lodash/round"; import { NATIVE_MINT } from "@solana/spl-token"; import { useSolBalance } from "@stream-swap/ui"; // Token Mints export const INPUT_MINT_ADDRESS = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC export const OUTPUT_MINT_ADDRESS = STR_MINT; // STR import { useJupiterApiContext } from "../../contexts"; interface IJupiterFormProps {} const JupiterForm: FunctionComponent = (props) => { const toastId = useRef(nanoid()); const [firstLoad, setFirstLoad] = useState(false); const { connected, publicKey, signAllTransactions, sendTransaction } = useWallet(); const { connection } = useConnection(); const { tokenMap, routeMap, loaded, api } = useJupiterApiContext(); const [routes, setRoutes] = useState< Awaited>["data"] >([]); const [slippage, setSlippage] = useLocalStorageState("slippage", { defaultValue: 1, }); const [selectedRoute, setSelectedRoute] = useState(null); const [inputTokenInfo, setInputTokenInfo] = useState< TokenInfo | null | undefined >(tokenMap.get(INPUT_MINT_ADDRESS) as TokenInfo); const [outputTokenInfo, setOutputTokenInfo] = useState< TokenInfo | null | undefined >(tokenMap.get(OUTPUT_MINT_ADDRESS) as TokenInfo); const [hasRoute, setHasRoute] = useState(false); const [swapping, setSwapping] = useState(false); const [loadingRoute, setLoadingRoute] = useState(true); // Loading by default const { data: tokenAccounts, refresh: refreshToken } = useTokenAccounts( connection, publicKey ); const [inputAmout, setInputAmount] = useState("1"); const { data: solBalance } = useSolBalance(connection, publicKey); useMemo(() => { setInputTokenInfo(tokenMap.get(INPUT_MINT_ADDRESS) as TokenInfo); setOutputTokenInfo(tokenMap.get(OUTPUT_MINT_ADDRESS) as TokenInfo); }, [tokenMap]); // Good to add debounce here to avoid multiple calls const fetchRoute = React.useCallback(() => { if (!inputTokenInfo || !outputTokenInfo) return; setLoadingRoute(true); api .v1QuoteGet({ amount: parseFloat(inputAmout) * Math.pow(10, inputTokenInfo?.decimals), inputMint: inputTokenInfo?.address, outputMint: outputTokenInfo?.address, slippage: slippage, feeBps: FEES_BPS, }) .then(({ data }) => { if (data) { setHasRoute( data.length > 0 && !!data[0].outAmount && data[0].outAmount > 0 ); setRoutes(data); } }) .finally(() => { setLoadingRoute(false); }); }, [api, inputAmout, inputTokenInfo, outputTokenInfo]); useEffect(() => { fetchRoute(); }, [fetchRoute]); const bestRoute = routes?.[0]; useEffect(() => { if (!firstLoad && bestRoute) { setSelectedRoute(bestRoute); setFirstLoad(true); } }, [loaded, bestRoute]); useEffect(() => { setFirstLoad(false); }, [inputTokenInfo, outputTokenInfo, loadingRoute]); // ensure outputMint can be swapable to inputMint useEffect(() => { if (inputTokenInfo) { const possibleOutputs = routeMap.get(inputTokenInfo.address); if ( possibleOutputs && !possibleOutputs?.includes(outputTokenInfo?.address || "") ) { setHasRoute(false); } } else { setHasRoute(false); } }, [inputTokenInfo, outputTokenInfo]); const handleSwap = async () => { if (!outputTokenInfo?.address) return; const wSol = inputTokenInfo?.address === NATIVE_MINT.toBase58(); const parsedAmount = parseFloat(inputAmout); if ( !parsedAmount || isNaN(parsedAmount) || !isFinite(parsedAmount) || !inputTokenInfo?.address ) { return toast.info(

Invalid amount

); } const tokenAccount = tokenAccounts?.getByMint( new PublicKey(inputTokenInfo?.address) ); const userBalances = wSol && solBalance ? solBalance.uiAmount : tokenAccount?.decimals ? Number(tokenAccount?.account.amount) / Math.pow(10, tokenAccount?.decimals) : null; if (!userBalances) { return toast.info(

Could not find user balances

); } if (userBalances < parsedAmount) { return toast.info(

Not enough balances (only have {round(userBalances, 2)} {inputTokenInfo.symbol})

); } const txids: string[] = []; try { if (!loadingRoute && selectedRoute && publicKey && signAllTransactions) { setSwapping(true); toast(, { type: toast.TYPE.INFO, autoClose: false, toastId: toastId.current, }); // Fee are in output token const { pubkey: feeAccount, ix } = await getFeeAddress( connection, new PublicKey(outputTokenInfo.address), publicKey ); let feeTx: Transaction | undefined = undefined; if (ix) { feeTx = new Transaction().add(ix); const { blockhash } = await connection.getLatestBlockhash(); feeTx.feePayer = publicKey; feeTx.recentBlockhash = blockhash; const sig = await sendTransaction(feeTx, connection); await connection.confirmTransaction(sig, "processed"); } const { swapTransaction, setupTransaction, cleanupTransaction } = await api.v1SwapPost({ body: { route: selectedRoute, userPublicKey: publicKey.toBase58(), feeAccount: feeAccount.toBase58(), }, }); const transactions = ( [setupTransaction, swapTransaction, cleanupTransaction].filter( Boolean ) as string[] ).map((tx) => { return Transaction.from(Buffer.from(tx, "base64")); }); toast.update(toastId.current, { type: toast.TYPE.INFO, autoClose: false, render: () => ( ), toastId: toastId.current, }); await signAllTransactions(transactions); let c = 1; const len = transactions.length; for (let transaction of transactions) { console.log(`Sending tx ${c}/${len}`); const txid = await retry(async () => { const sig = await sendRawTransaction( [connection, GENESYS_GO_CONNECTIONS], transaction ); await connection.confirmTransaction(sig, "processed"); return sig; }); txids.push(txid); console.log(txid); c += 1; } toast.update(toastId.current, { type: toast.TYPE.SUCCESS, autoClose: 5_000, render: () => ( ), toastId: toastId.current, }); } } catch (e) { console.error("Error", e); const isError = e instanceof Error; if (isError && e.message.includes("Transaction simulation")) { toast.update(toastId.current, { type: toast.TYPE.INFO, autoClose: 5_000, render: () => ( ), }); } else if (isError && e.message.includes("blockhash")) { toast.update(toastId.current, { type: toast.TYPE.INFO, autoClose: 5_000, render: () => ( ), toastId: toastId.current, }); } else if ( isError && e.message.includes("Transaction was not confirmed") && txids.length > 0 ) { toast.update(toastId.current, { type: toast.TYPE.INFO, autoClose: 5_000, render: () => ( ), }); } else if ( isError && e.message.includes("Transaction was not confirmed") ) { toast.update(toastId.current, { type: toast.TYPE.INFO, autoClose: 5_000, render: () => ( ), }); } else { toast.update(toastId.current, { type: toast.TYPE.ERROR, autoClose: 5_000, render: () => ( ), }); } } refreshToken(); setSwapping(false); }; const handleSwitch = () => { const _ = { ...inputTokenInfo } as TokenInfo; setInputTokenInfo(outputTokenInfo); setOutputTokenInfo(_); }; const outputAmount = bestRoute && (bestRoute.outAmount || 0) / Math.pow(10, outputTokenInfo?.decimals || 1); const refresh = async () => { if (swapping) return; fetchRoute(); refreshToken(); }; useInterval(() => { refresh(); }, 15_000); return ( <>
You pay
setInputAmount(e.target.value.trim())} className="absolute text-xl font-bold text-right bg-transparent right-4 top-4 input focus:outline-0" />
You receive
{outputAmount}
{loadingRoute && (
)} {!hasRoute && !loadingRoute && (
No route found
)} {!loadingRoute && !!bestRoute && !!bestRoute.marketInfos && !!outputAmount && !!hasRoute && ( )} {!loadingRoute && hasRoute && routes ?.slice(1) ?.filter((e) => !!e.marketInfos && !!e.outAmount) .map((r, idx) => { return ( ); })} {connected ? (
{swapping ? (
Swapping
) : ( "Swap" )}
) : (
)}
); }; export default JupiterForm;