import { OPENBOOK_PROGRAM_ID } from '@blockworks-foundation/mango-v4' import Input from '@components/forms/Input' import Label from '@components/forms/Label' import Select from '@components/forms/Select' import CreateOpenbookMarketModal from '@components/modals/CreateOpenbookMarketModal' import Button, { IconButton } from '@components/shared/Button' import Loading from '@components/shared/Loading' import { Disclosure } from '@headlessui/react' import { ArrowLeftIcon, ChevronDownIcon, ExclamationCircleIcon, ExclamationTriangleIcon, } from '@heroicons/react/20/solid' import { useWallet } from '@solana/wallet-adapter-react' import { PublicKey } from '@solana/web3.js' import GovernanceStore from '@store/governanceStore' import mangoStore, { CLUSTER } from '@store/mangoStore' import useMangoGroup from 'hooks/useMangoGroup' import { useTranslation } from 'next-i18next' import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' import { MANGO_DAO_WALLET, MANGO_DAO_WALLET_GOVERNANCE, } from 'utils/governance/constants' import { createProposal } from 'utils/governance/instructions/createProposal' import { getBestMarket } from 'utils/governance/listingTools' import { notify } from 'utils/notifications' import ListingSuccess from '../ListingSuccess' import { formatTokenSymbol } from 'utils/tokens' import OnBoarding from '../OnBoarding' import { tryGetPubKey } from 'utils/governance/tools' import { calculateMarketTradingParams } from '@blockworks-foundation/mango-v4-settings/lib/helpers/listingTools' type FormErrors = Partial> type ListMarketForm = { openBookMarketExternalPk: string proposalTitle: string proposalDescription: string marketIndex: number marketName: string } enum VIEWS { BASE_TOKEN, PROPS, SUCCESS, } const defaultFormValues: ListMarketForm = { openBookMarketExternalPk: '', proposalDescription: '', proposalTitle: '', marketIndex: 0, marketName: '', } const ListMarket = ({ goBack }: { goBack: () => void }) => { //do not deconstruct wallet is used for anchor to sign const wallet = useWallet() const { t } = useTranslation(['governance', 'trade']) const { group } = useMangoGroup() const connection = mangoStore((s) => s.connection) const client = mangoStore((s) => s.client) const voter = GovernanceStore((s) => s.voter) const vsrClient = GovernanceStore((s) => s.vsrClient) const proposals = GovernanceStore((s) => s.proposals) const proposalsLoading = GovernanceStore((s) => s.loadingProposals) const [advForm, setAdvForm] = useState({ ...defaultFormValues, }) const [proposalPk, setProposalPk] = useState(null) const [formErrors, setFormErrors] = useState({}) const [baseToken, setBaseToken] = useState(null) const [quoteToken, setQuoteToken] = useState(null) const [loadingMarketProps, setLoadingMarketProps] = useState(false) const [proposing, setProposing] = useState(false) const [marketPk, setMarketPk] = useState('') const [currentView, setCurrentView] = useState(VIEWS.BASE_TOKEN) const [createOpenbookMarketModal, setCreateOpenbookMarket] = useState(false) const baseBanks = baseToken ? group?.banksMapByName.get(baseToken) : null const quoteBanks = quoteToken ? group?.banksMapByName.get(quoteToken) : null const baseBank = baseBanks?.length ? baseBanks[0] : null const quoteBank = quoteBanks?.length ? quoteBanks[0] : null const marketName = `${baseToken?.toUpperCase()}/${quoteToken?.toUpperCase()}` const [baseTokens, quoteTokens] = useMemo(() => { if (!group) return [[], []] const allTokens = [...group.banksMapByName.keys()].sort((a, b) => a.localeCompare(b), ) return [ allTokens.filter((t) => t !== quoteToken), allTokens.filter((t) => t !== baseToken), ] }, [baseToken, group, quoteToken]) const handleSetAdvForm = (propertyName: string, value: string | number) => { setFormErrors({}) setAdvForm({ ...advForm, [propertyName]: value }) } const openCreateOpenbookMarket = () => { setCreateOpenbookMarket(true) } const closeCreateOpenBookMarketModal = () => { setCreateOpenbookMarket(false) handleGetMarketProps() } const goToHomePage = async () => { setFormErrors({}) setCurrentView(VIEWS.BASE_TOKEN) setMarketPk('') setProposalPk('') setAdvForm({ ...defaultFormValues, }) } const isFormValid = useCallback( (advForm: ListMarketForm) => { const invalidFields: FormErrors = {} setFormErrors({}) const pubkeyFields: (keyof ListMarketForm)[] = [ 'openBookMarketExternalPk', ] const numberFields: (keyof ListMarketForm)[] = ['marketIndex'] const textFields: (keyof ListMarketForm)[] = [ 'marketName', 'proposalTitle', ] for (const key of pubkeyFields) { if (!tryGetPubKey(advForm[key] as string)) { invalidFields[key] = t('invalid-pk') } } for (const key of numberFields) { if (isNaN(advForm[key] as number) || advForm[key] === '') { invalidFields[key] = t('invalid-num') } } for (const key of textFields) { if (!advForm[key]) { invalidFields[key] = t('field-req') } } if (Object.keys(invalidFields).length) { setFormErrors(invalidFields) } return invalidFields }, [t], ) const handlePropose = useCallback(async () => { const invalidFields = isFormValid(advForm) if (Object.keys(invalidFields).length) { return } setProposing(true) const index = proposals ? Object.values(proposals).length : 0 const proposalTx = [] const registerMarketix = await client!.program.methods .serum3RegisterMarket(advForm.marketIndex, advForm.marketName) .accounts({ group: group!.publicKey, admin: MANGO_DAO_WALLET, serumProgram: OPENBOOK_PROGRAM_ID[CLUSTER], serumMarketExternal: new PublicKey(advForm.openBookMarketExternalPk), baseBank: baseBank!.publicKey, quoteBank: quoteBank!.publicKey, payer: MANGO_DAO_WALLET, }) .instruction() proposalTx.push(registerMarketix) const walletSigner = wallet as never try { const proposalAddress = await createProposal( connection, walletSigner, MANGO_DAO_WALLET_GOVERNANCE, voter.tokenOwnerRecord!, advForm.proposalTitle, advForm.proposalDescription, index, proposalTx, vsrClient!, ) setProposalPk(proposalAddress.toBase58()) setCurrentView(VIEWS.SUCCESS) } catch (e) { notify({ title: t('error-proposal-creation'), description: `${e}`, type: 'error', }) } setProposing(false) }, [ advForm, baseBank, client, connection, group, isFormValid, proposals, quoteBank, t, voter.tokenOwnerRecord, vsrClient, wallet, ]) const goToPropsPage = async () => { await handleGetMarketProps() setCurrentView(VIEWS.PROPS) } const handleGetMarketProps = useCallback(async () => { if (!baseBank || !quoteBank) { return } setLoadingMarketProps(true) const [bestMarketPk] = await Promise.all([ getBestMarket({ baseMint: baseBank.mint.toBase58(), quoteMint: quoteBank.mint.toBase58(), cluster: CLUSTER, connection, }), ]) setMarketPk(bestMarketPk?.toBase58() || '') setLoadingMarketProps(false) }, [baseBank, quoteBank, connection]) useEffect(() => { const index = proposals ? Object.values(proposals).length : 0 setAdvForm((prevForm) => ({ ...prevForm, marketIndex: Number(index), marketName: marketName, proposalTitle: `List market ${marketName}`, openBookMarketExternalPk: marketPk, })) }, [marketName, proposals, marketPk]) const tradingParams = useMemo(() => { if (baseBank && quoteBank) { return calculateMarketTradingParams( baseBank.uiPrice, quoteBank.uiPrice, baseBank.mintDecimals, quoteBank.mintDecimals, ) } return { baseLots: 0, quoteLots: 0, minOrderValue: 0, baseLotExponent: 0, quoteLotExponent: 0, minOrderSize: 0, priceIncrement: 0, priceIncrementRelative: 0, } }, [baseBank, quoteBank]) return (

{t('list-spot-market')}

{proposalPk && currentView === VIEWS.SUCCESS ? ( ) : null} {currentView === VIEWS.BASE_TOKEN ? (

{t('market-pair')}{' '} {baseToken && quoteToken ? `- ${formatTokenSymbol(baseToken)}/${formatTokenSymbol( quoteToken, )}` : null}

) : null} {currentView === VIEWS.PROPS ? (
{!marketPk ? ( <>

{t('no-openbook-found', { market: `${baseToken}/${quoteToken}`, })}

{t('no-openbook-found-desc')}

{createOpenbookMarketModal ? ( ) : null} ) : ( <>

{t('market-name')}

{t('market-name-desc')}

  • {t('market-name-convention-1')}
  • {t('market-name-convention-2')}
  • {t('market-name-convention-3')}
  • {t('market-name-convention-4')}
  • {t('market-name-convention-5')}
  • {t('market-name-convention-6')} Discord

{t('trade:market-details', { market: '' })}

{tradingParams.minOrderSize ? (

{t('min-order')}

{tradingParams.minOrderSize}

) : null} {tradingParams.minOrderSize ? (

{t('price-tick')}

{tradingParams.priceIncrement}

) : null}
)} {marketPk ? ( {({ open }) => ( <>
{t('adv-fields')}
{formErrors.marketName && (

{formErrors.marketName}

)}
)}
) : null}
{wallet.connected ? ( ) : ( )}
) : null}
) } export default ListMarket