import { Avatar, Box, Button, Center, Divider, Flex, HStack, Icon, IconButton, Input, InputGroup, InputRightElement, Link, Menu, MenuButton, MenuItem, MenuList, ScaleFade, Text, Tooltip, useColorModeValue, VStack, } from "@chakra-ui/react"; import { yupResolver } from "@hookform/resolvers/yup"; import { NATIVE_MINT } from "@solana/spl-token"; import { useWallet } from "@solana/wallet-adapter-react"; import { PublicKey } from "@solana/web3.js"; import { BondingHierarchy, BondingPricing, } from "../../../../spl-token-bonding"; import React, { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { BsChevronDown } from "react-icons/bs"; import { RiArrowUpDownFill, RiInformationLine } from "react-icons/ri"; import * as yup from "yup"; import { useFtxPayLink } from "../../hooks/useFtxPayLink"; import { useMint } from "../../hooks/useMint"; import { useProvider } from "../../hooks/useProvider"; import { useSolanaUnixTime } from "../../hooks/useSolanaUnixTime"; import { useTokenMetadata } from "../../hooks/useTokenMetadata"; import { useTokenSwapFromId } from "../../hooks/useTokenSwapFromId"; import { useTwWrappedSolMint } from "../../hooks/useTwWrappedSolMint"; import { roundToDecimals } from "../../utils/roundToDecimals"; import { Spinner } from "../Spinner"; import { Royalties } from "./Royalties"; import { TransactionInfo, TransactionInfoArgs } from "./TransactionInfo"; export interface ISwapFormValues { topAmount: number; bottomAmount: number; slippage: number; lastSet: "bottom" | "top"; } const validationSchema = yup .object({ topAmount: yup.number().required().moreThan(0), bottomAmount: yup.number().required().moreThan(0), slippage: yup.number().required().moreThan(0), }) .required(); export interface ISwapFormProps { isLoading?: boolean; isSubmitting: boolean; isBuying: boolean; onConnectWallet: () => void; onTradingMintsChange: (args: { base: PublicKey; target: PublicKey }) => void; onBuyBase?: (tokenBonding: PublicKey) => void; onSubmit: (values: ISwapFormValues) => Promise; goLiveDate: Date | undefined; id: PublicKey | undefined; pricing: BondingPricing | undefined; baseOptions: PublicKey[]; targetOptions: PublicKey[]; base: | { name: string; ticker: string; image: string | undefined; publicKey: PublicKey; } | undefined; target: | { name: string; ticker: string; image: string | undefined; publicKey: PublicKey; } | undefined; ownedBase: number | undefined; spendCap: number; mintCap?: number; numRemaining?: number; feeAmount?: number; showAttribution?: boolean; extraTransactionInfo?: Omit[]; swapBaseWithTargetEnabled?: boolean; } function MintMenuItem({ mint, onClick, }: { mint: PublicKey; onClick: () => void; }) { const { image, metadata } = useTokenMetadata(mint); return ( } > {metadata?.data.symbol} ); } export const SwapForm = ({ isLoading = false, extraTransactionInfo, isSubmitting, onConnectWallet, onTradingMintsChange, onBuyBase, onSubmit, id, pricing, base, target, ownedBase, spendCap, feeAmount, baseOptions, targetOptions, mintCap, isBuying, goLiveDate, numRemaining, showAttribution = true, swapBaseWithTargetEnabled = true, }: ISwapFormProps) => { const formRef = useRef() as React.MutableRefObject; const { connected } = useWallet(); const { awaitingApproval } = useProvider(); const ftxPayLink = useFtxPayLink(); const [insufficientLiq, setInsufficientLiq] = useState(false); const [rate, setRate] = useState("--"); const [fee, setFee] = useState("--"); const notLive = goLiveDate && new Date() < goLiveDate; const { register, handleSubmit, watch, reset, setValue, formState: { errors }, } = useForm({ defaultValues: { topAmount: undefined, bottomAmount: undefined, slippage: 1, }, // @ts-ignore resolver: yupResolver(validationSchema), }); const wrappedSolMint = useTwWrappedSolMint(); const isBaseSol = wrappedSolMint && (base?.publicKey.equals(wrappedSolMint) || base?.publicKey.equals(NATIVE_MINT)); const topAmount = watch("topAmount"); const bottomAmount = watch("bottomAmount"); const slippage = watch("slippage"); const hasBaseAmount = (ownedBase || 0) >= +(topAmount || 0); const moreThanSpendCap = +(topAmount || 0) > spendCap; const unixTime = useSolanaUnixTime(); const { tokenBonding, childEntangler, parentEntangler } = useTokenSwapFromId(id); const passedMintCap = typeof numRemaining !== "undefined" && numRemaining < bottomAmount; const targetMintAcc = useMint(target?.publicKey); const baseMintAcc = useMint(base?.publicKey); const handleConnectWallet = () => onConnectWallet(); const manualResetForm = () => { reset({ slippage: slippage }); setInsufficientLiq(false); setRate("--"); setFee("--"); }; const [lastSet, setLastSet] = useState<"bottom" | "top">("top"); function updatePrice() { if (lastSet == "bottom" && bottomAmount) { handleBottomChange(bottomAmount); } else if (topAmount) { handleTopChange(topAmount); } } useEffect(() => { updatePrice(); }, [pricing, bottomAmount, topAmount, targetMintAcc, baseMintAcc, unixTime]); const handleTopChange = (value: number | undefined = 0) => { if (tokenBonding && pricing && base && target && value && +value >= 0) { setLastSet("top"); const amount = pricing.swap( +value, base.publicKey, target.publicKey, true, unixTime ); if (isNaN(amount)) { setInsufficientLiq(true); } else { setInsufficientLiq(false); setValue( "bottomAmount", +value == 0 ? 0 : roundToDecimals( amount, targetMintAcc ? targetMintAcc.decimals : 9 ) ); setRate( `${roundToDecimals( amount / value, targetMintAcc ? targetMintAcc.decimals : 9 )}` ); setFee(`${feeAmount}`); } } else { manualResetForm(); } }; const handleBottomChange = (value: number | undefined = 0) => { if (tokenBonding && pricing && base && target && value && +value >= 0) { let amount = Math.abs( pricing.swapTargetAmount( +value, target.publicKey, base.publicKey, true, unixTime ) ); setLastSet("bottom"); if (isNaN(amount)) { setInsufficientLiq(true); } else { setInsufficientLiq(false); setValue( "topAmount", +value == 0 ? 0 : roundToDecimals(amount, baseMintAcc ? baseMintAcc.decimals : 9) ); setRate( `${roundToDecimals( value / amount, baseMintAcc ? baseMintAcc.decimals : 9 )}` ); setFee(`${feeAmount}`); } } else { manualResetForm(); } }; const attColor = useColorModeValue("gray.400", "gray.200"); const dropdownVariant = useColorModeValue("solid", "ghost"); const swapBackground = useColorModeValue("gray.200", "gray.500"); const color = useColorModeValue("gray.500", "gray.200"); const inputBorderColor = useColorModeValue("gray.200", "gray.500"); const useMaxBg = useColorModeValue("primary.200", "black.500"); const handleUseMax = () => { const amount = (ownedBase || 0) >= spendCap ? spendCap : ownedBase || 0; setValue("topAmount", amount); handleTopChange(amount); }; const handleFlipTokens = () => { if (base && target) { onTradingMintsChange({ base: target.publicKey, target: base.publicKey, }); } }; const handleBuyBase = onBuyBase ? () => onBuyBase(tokenBonding!.publicKey) : isBaseSol ? () => window.open(ftxPayLink) : undefined; const handleSwap = async (values: ISwapFormValues) => { await onSubmit({ ...values, lastSet }); }; if (isLoading || !base || !target) { return ; } return (
You Pay {base && handleBuyBase && ( Buy More {base.ticker} )} handleTopChange(e.target.value), })} /> {connected && ( 0 ? : null } leftIcon={
} borderRadius="20px 6px 6px 20px" paddingX={1.5} > {base.ticker}
{baseOptions.map((mint) => ( onTradingMintsChange({ base: mint, target: target.publicKey && mint.equals(target.publicKey) ? base.publicKey : target.publicKey, }) } /> ))}
)}
{!connected && ( )} {connected && ( )} {swapBaseWithTargetEnabled && ( } /> )} You Receive handleBottomChange(e.target.value), })} /> {connected && ( 0 ? : null } isDisabled={!connected} as={Button} leftIcon={
} borderRadius="20px 6px 6px 20px" paddingX={1.5} > {target.ticker}
{targetOptions.map((mint) => ( onTradingMintsChange({ target: mint, base: base.publicKey && mint.equals(base.publicKey) ? target.publicKey : base.publicKey, }) } /> ))}
)}
Rate {rate !== "--" ? `1 ${base.ticker} ≈ ${rate} ${target.ticker}` : rate} Slippage % Solana Network Fees {fee} {numRemaining && ( Remaining {numRemaining} / {mintCap} )} {base && target && pricing?.hierarchy .path(base.publicKey, target.publicKey) .map((h: BondingHierarchy, idx: number) => ( ))} {(extraTransactionInfo || []).map((i) => ( ))}
{passedMintCap && ( {(numRemaining || 0) > 0 ? `Only ${numRemaining} left` : "Sold Out"} )} {moreThanSpendCap && ( You cannot buy more than {spendCap} {base.ticker} at a time. )} {notLive && ( Goes live at {goLiveDate && goLiveDate.toLocaleString()} )} {!hasBaseAmount && ( Insufficient funds for this trade.{" "} {`Buy more now.`} )} {/* Make sure this doesn't render at the same time as the above insufficient funds */} {insufficientLiq && hasBaseAmount && ( Insufficient Liqidity for this trade. )}
{!connected && ( )} {connected && ( )}
{showAttribution && (
Powered by Strata
)}
); }; export const MemodSwapForm = React.memo(SwapForm); function getStep(arg0: number): string { return arg0 == 0 ? "1" : "0." + "0".repeat(Math.abs(arg0) - 1) + "1"; }