import Input from '@components/forms/Input' import Label from '@components/forms/Label' import Button, { IconButton } from '@components/shared/Button' import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' import mangoStore, { CLUSTER } from '@store/mangoStore' import { Token } from 'types/jupiter' import { handleGetRoutes } from '@components/swap/useQuoteRoutes' import { JUPITER_API_DEVNET, JUPITER_API_MAINNET, USDC_MINT, } from 'utils/constants' import { PublicKey, SYSVAR_RENT_PUBKEY } from '@solana/web3.js' import { useWallet } from '@solana/wallet-adapter-react' import { OPENBOOK_PROGRAM_ID } from '@blockworks-foundation/mango-v4' import { MANGO_DAO_WALLET, MANGO_DAO_WALLET_GOVERNANCE, MANGO_MINT_DECIMALS, } from 'utils/governance/constants' import { ArrowLeftIcon, ChevronDownIcon, ExclamationCircleIcon, } from '@heroicons/react/20/solid' import BN from 'bn.js' import { createProposal } from 'utils/governance/instructions/createProposal' import GovernanceStore from '@store/governanceStore' import { notify } from 'utils/notifications' import { useTranslation } from 'next-i18next' import { emptyPk } from 'utils/governance/accounts/vsrAccounts' import Loading from '@components/shared/Loading' import ListingSuccess from '../ListingSuccess' import InlineNotification from '@components/shared/InlineNotification' import { Disclosure } from '@headlessui/react' import { useEnhancedWallet } from '@components/wallet/EnhancedWalletProvider' import { abbreviateAddress } from 'utils/formatting' import { formatNumericValue } from 'utils/numbers' import useMangoGroup from 'hooks/useMangoGroup' import { getBestMarket, getOracle } from 'utils/governance/listingTools' import { fmtTokenAmount, tryGetPubKey } from 'utils/governance/tools' import OnBoarding from '../OnBoarding' type FormErrors = Partial> type TokenListForm = { mintPk: string oraclePk: string name: string tokenIndex: number openBookMarketExternalPk: string baseBankPk: string quoteBankPk: string marketIndex: number openBookProgram: string marketName: string proposalTitle: string proposalDescription: string } const defaultTokenListFormValues: TokenListForm = { mintPk: '', oraclePk: '', name: '', tokenIndex: 0, openBookMarketExternalPk: '', baseBankPk: '', quoteBankPk: '', marketIndex: 0, openBookProgram: '', marketName: '', proposalTitle: '', proposalDescription: '', } const ListToken = ({ goBack }: { goBack: () => void }) => { const wallet = useWallet() const connection = mangoStore((s) => s.connection) const client = mangoStore((s) => s.client) const { group } = useMangoGroup() const { handleConnect } = useEnhancedWallet() const voter = GovernanceStore((s) => s.voter) const vsrClient = GovernanceStore((s) => s.vsrClient) const governances = GovernanceStore((s) => s.governances) const loadingRealm = GovernanceStore((s) => s.loadingRealm) const loadingVoter = GovernanceStore((s) => s.loadingVoter) const proposals = GovernanceStore((s) => s.proposals) const getCurrentVotingPower = GovernanceStore((s) => s.getCurrentVotingPower) const connectionContext = GovernanceStore((s) => s.connectionContext) const { t } = useTranslation(['governance']) const [advForm, setAdvForm] = useState({ ...defaultTokenListFormValues, }) const [loadingListingParams, setLoadingListingParams] = useState(false) const [tokenList, setTokenList] = useState([]) const [formErrors, setFormErrors] = useState({}) const [priceImpact, setPriceImpact] = useState(0) const [currentTokenInfo, setCurrentTokenInfo] = useState< Token | null | undefined >(null) const [proposalPk, setProposalPk] = useState(null) const [mint, setMint] = useState('') const [creatingProposal, setCreatingProposal] = useState(false) const minVoterWeight = useMemo( () => governances ? governances[MANGO_DAO_WALLET_GOVERNANCE.toBase58()].account.config .minCommunityTokensToCreateProposal : new BN(0), [governances] ) const mintVoterWeightNumber = governances ? fmtTokenAmount(minVoterWeight, MANGO_MINT_DECIMALS) : 0 const handleSetAdvForm = (propertyName: string, value: string | number) => { setFormErrors({}) setAdvForm({ ...advForm, [propertyName]: value }) } const handleTokenFind = async () => { cancel() if (!tryGetPubKey(mint)) { notify({ title: t('enter-valid-token-mint'), type: 'error', }) return } let currentTokenList: Token[] = tokenList if (!tokenList.length) { currentTokenList = await getTokenList() setTokenList(currentTokenList) } const tokenInfo = currentTokenList.find((x) => x.address === mint) setCurrentTokenInfo(tokenInfo) if (tokenInfo) { handleLiqudityCheck(new PublicKey(mint)) getListingParams(tokenInfo) } } const getTokenList = useCallback(async () => { try { const url = CLUSTER === 'devnet' ? JUPITER_API_DEVNET : JUPITER_API_MAINNET const response = await fetch(url) const data: Token[] = await response.json() return data } catch (e) { notify({ title: t('cant-find-token-for-mint'), description: `${e}`, type: 'error', }) return [] } }, [t]) const getListingParams = useCallback( async (tokenInfo: Token) => { setLoadingListingParams(true) const [oraclePk, marketPk] = await Promise.all([ getOracle({ baseSymbol: tokenInfo.symbol, quoteSymbol: 'usd', connection, }), getBestMarket({ baseMint: mint, quoteMint: USDC_MINT, cluster: CLUSTER, connection, }), ]) const index = proposals ? Object.values(proposals).length : 0 const bankNum = 0 const [baseBank] = PublicKey.findProgramAddressSync( [ Buffer.from('Bank'), group!.publicKey.toBuffer(), new BN(index).toArrayLike(Buffer, 'le', 2), new BN(bankNum).toArrayLike(Buffer, 'le', 4), ], client.programId ) setAdvForm({ ...advForm, oraclePk: oraclePk || '', mintPk: mint, name: tokenInfo.symbol, tokenIndex: index, openBookProgram: OPENBOOK_PROGRAM_ID[CLUSTER].toBase58(), marketName: `${tokenInfo.symbol}/USDC`, baseBankPk: baseBank.toBase58(), quoteBankPk: group! .getFirstBankByMint(new PublicKey(USDC_MINT)) .publicKey.toBase58(), marketIndex: index, openBookMarketExternalPk: marketPk?.toBase58() || '', proposalTitle: `List ${tokenInfo.symbol} on Mango-v4`, }) setLoadingListingParams(false) }, [advForm, client.programId, connection, group, mint, proposals] ) const handleLiqudityCheck = useCallback( async (tokenMint: PublicKey) => { try { //we check price impact on token for 10k USDC const USDC_AMOUNT = 10000000000 const SLIPPAGE_BPS = 50 const MODE = 'ExactIn' const FEE = 0 const { bestRoute } = await handleGetRoutes( USDC_MINT, tokenMint.toBase58(), USDC_AMOUNT, SLIPPAGE_BPS, MODE, FEE, wallet.publicKey ? wallet.publicKey?.toBase58() : emptyPk ) setPriceImpact(bestRoute ? bestRoute.priceImpactPct * 100 : 100) } catch (e) { notify({ title: t('liquidity-check-error'), description: `${e}`, type: 'error', }) } }, [t, wallet.publicKey] ) const cancel = () => { setCurrentTokenInfo(null) setPriceImpact(0) setAdvForm({ ...defaultTokenListFormValues }) setProposalPk(null) } const isFormValid = useCallback( (advForm: TokenListForm) => { const invalidFields: FormErrors = {} setFormErrors({}) const pubkeyFields: (keyof TokenListForm)[] = [ 'openBookProgram', 'quoteBankPk', 'baseBankPk', 'openBookMarketExternalPk', 'oraclePk', ] const numberFields: (keyof TokenListForm)[] = ['tokenIndex'] const textFields: (keyof TokenListForm)[] = [ '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 propose = useCallback(async () => { const invalidFields = isFormValid(advForm) if (Object.keys(invalidFields).length) { return } if (!wallet?.publicKey || !vsrClient || !connectionContext) return await getCurrentVotingPower(wallet.publicKey, vsrClient, connectionContext) if (voter.voteWeight.cmp(minVoterWeight) === -1) { notify({ title: `${t('on-boarding-description', { amount: formatNumericValue(mintVoterWeightNumber), })} ${t('mango-governance')}`, type: 'error', }) return } const proposalTx = [] const registerTokenIx = await client!.program.methods .tokenRegisterTrustless(Number(advForm.tokenIndex), advForm.name) .accounts({ admin: MANGO_DAO_WALLET, group: group!.publicKey, mint: new PublicKey(advForm.mintPk), oracle: new PublicKey(advForm.oraclePk), payer: MANGO_DAO_WALLET, rent: SYSVAR_RENT_PUBKEY, }) .instruction() proposalTx.push(registerTokenIx) const registerMarketix = await client!.program.methods .serum3RegisterMarket(Number(advForm.marketIndex), advForm.marketName) .accounts({ group: group!.publicKey, admin: MANGO_DAO_WALLET, serumProgram: new PublicKey(advForm.openBookProgram), serumMarketExternal: new PublicKey(advForm.openBookMarketExternalPk), baseBank: new PublicKey(advForm.baseBankPk), quoteBank: new PublicKey(advForm.quoteBankPk), payer: MANGO_DAO_WALLET, }) .instruction() proposalTx.push(registerMarketix) const walletSigner = wallet as never setCreatingProposal(true) try { const proposalAddress = await createProposal( connection, walletSigner, MANGO_DAO_WALLET_GOVERNANCE, voter.tokenOwnerRecord!, advForm.proposalTitle, advForm.proposalDescription, advForm.tokenIndex, proposalTx, vsrClient ) setProposalPk(proposalAddress.toBase58()) } catch (e) { notify({ title: t('error-proposal-creation'), description: `${e}`, type: 'error', }) } setCreatingProposal(false) }, [ advForm, client, connection, connectionContext, getCurrentVotingPower, group, isFormValid, minVoterWeight, mintVoterWeightNumber, t, voter.tokenOwnerRecord, voter.voteWeight, vsrClient, wallet, ]) useEffect(() => { setTokenList([]) }, []) return (

{t('list-token')}

{!currentTokenInfo ? ( <>
) : ( <> {proposalPk ? ( ) : ( <>

{t('token-details')}

{t('name')}

{currentTokenInfo?.name}

{t('symbol')}

{currentTokenInfo?.symbol}

{t('mint')}

{abbreviateAddress( new PublicKey(currentTokenInfo?.address) )}

{priceImpact > 2 && (
)}
{({ open }) => ( <>
{t('adv-fields')}
)}
{!advForm.oraclePk && !loadingListingParams ? (
) : null} {!advForm.openBookMarketExternalPk && !loadingListingParams ? ( } type="error" />
) : null}
{wallet.connected ? ( ) : ( )}
)} )} ) } export default ListToken