From 40bc4f7d789ac12367bd77c6c0637292e184aa59 Mon Sep 17 00:00:00 2001 From: philippe-ftx <62417741+philippe-ftx@users.noreply.github.com> Date: Thu, 24 Sep 2020 13:13:23 +0200 Subject: [PATCH] Custom markets input (#15) Co-authored-by: Nishad --- src/components/CustomMarketDialog.jsx | 237 ++++++++++++++++++++++++++ src/pages/TradePage.jsx | 166 +++++++++++++----- src/utils/markets.js | 40 ++++- src/utils/utils.js | 13 ++ 4 files changed, 412 insertions(+), 44 deletions(-) create mode 100644 src/components/CustomMarketDialog.jsx diff --git a/src/components/CustomMarketDialog.jsx b/src/components/CustomMarketDialog.jsx new file mode 100644 index 0000000..806db20 --- /dev/null +++ b/src/components/CustomMarketDialog.jsx @@ -0,0 +1,237 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Input, Row, Col, Typography } from 'antd'; +import { notify } from '../utils/notifications'; +import { isValidPublicKey } from '../utils/utils'; +import { PublicKey } from '@solana/web3.js'; +import { Market, MARKETS, TOKEN_MINTS } from '@project-serum/serum'; +import { useConnection } from '../utils/connection'; +import { LoadingOutlined } from '@ant-design/icons'; + +const { Text } = Typography; + +export default function CustomMarketDialog({ + visible, + onAddCustomMarket, + onClose, +}) { + const connection = useConnection(); + const [marketId, setMarketId] = useState(null); + const [programId, setProgramId] = useState( + MARKETS.find(({ deprecated }) => !deprecated)?.programId?.toBase58(), + ); + + const [marketLabel, setMarketLabel] = useState(null); + const [baseLabel, setBaseLabel] = useState(null); + const [quoteLabel, setQuoteLabel] = useState(null); + + const [market, setMarket] = useState(null); + const [loadingMarket, setLoadingMarket] = useState(false); + + const wellFormedMarketId = isValidPublicKey(marketId); + const wellFormedProgramId = isValidPublicKey(programId); + + useEffect(() => { + if (!wellFormedMarketId || !wellFormedProgramId) { + resetLabels(); + return; + } + setLoadingMarket(true); + Market.load( + connection, + new PublicKey(marketId), + {}, + new PublicKey(programId), + ) + .then((market) => { + setMarket(market); + }) + .catch(() => { + resetLabels(); + setMarket(null); + }) + .finally(() => setLoadingMarket(false)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connection, marketId, programId]); + + const resetLabels = () => { + setMarketLabel(null); + setBaseLabel(null); + setQuoteLabel(null); + }; + + const knownMarket = MARKETS.find( + (m) => + m.address.toBase58() === marketId && m.programId.toBase58() === programId, + ); + const knownProgram = MARKETS.find( + (m) => m.programId.toBase58() === programId, + ); + const knownBaseCurrency = + market?.baseMintAddress && + TOKEN_MINTS.find((token) => token.address.equals(market.baseMintAddress)) + ?.name; + + const knownQuoteCurrency = + market?.quoteMintAddress && + TOKEN_MINTS.find((token) => token.address.equals(market.quoteMintAddress)) + ?.name; + + const canSubmit = + !loadingMarket && + !!market && + marketId && + programId && + marketLabel && + (knownBaseCurrency || baseLabel) && + (knownQuoteCurrency || quoteLabel) && + wellFormedProgramId && + wellFormedMarketId; + + const onSubmit = () => { + if (!canSubmit) { + notify({ + message: 'Please fill in all fields with valid values', + type: 'error', + }); + return; + } + + let params = { + address: marketId, + programId, + name: marketLabel, + }; + if (!knownBaseCurrency) { + params.baseLabel = baseLabel; + } + if (!knownQuoteCurrency) { + params.quoteLabel = quoteLabel; + } + onAddCustomMarket(params); + onDoClose(); + }; + + const onDoClose = () => { + resetLabels(); + setMarket(null); + setMarketId(null); + setProgramId(null); + onClose(); + }; + + return ( + +
+ {wellFormedMarketId && wellFormedProgramId ? ( + <> + {!market && ( + + + Not a valid market and program ID combination + + + )} + {market && knownMarket && ( + + Market known: {knownMarket.name} + + )} + {market && !knownProgram && ( + + Warning: unknown DEX program + + )} + {market && knownProgram && knownProgram.deprecated && ( + + Warning: deprecated DEX program + + )} + + ) : ( + <> + {marketId && !wellFormedMarketId && ( + + Invalid market ID + + )} + {marketId && !wellFormedProgramId && ( + + Invalid program ID + + )} + + )} + + + setMarketId(e.target.value)} + suffix={loadingMarket ? : null} + /> + + + + {programId && !isValidPublicKey(programId) && ( + + Invalid program ID + + )} + + + setProgramId(e.target.value)} + /> + + + + + setMarketLabel(e.target.value)} + /> + + + + + setBaseLabel(e.target.value)} + /> + {market && !knownBaseCurrency && ( +
+ Warning: unknown token +
+ )} + + + setQuoteLabel(e.target.value)} + /> + {market && !knownQuoteCurrency && ( +
+ Warning: unknown token +
+ )} + +
+
+
+ ); +} diff --git a/src/pages/TradePage.jsx b/src/pages/TradePage.jsx index 3618aaf..24a9ec6 100644 --- a/src/pages/TradePage.jsx +++ b/src/pages/TradePage.jsx @@ -8,14 +8,21 @@ import { useMarket, useMarketsList, useUnmigratedDeprecatedMarkets, + getMarketInfos, } from '../utils/markets'; import TradeForm from '../components/TradeForm'; import TradesTable from '../components/TradesTable'; import LinkAddress from '../components/LinkAddress'; import DeprecatedMarketsInstructions from '../components/DeprecatedMarketsInstructions'; -import { InfoCircleOutlined } from '@ant-design/icons'; +import { + InfoCircleOutlined, + PlusCircleOutlined, + DeleteOutlined, +} from '@ant-design/icons'; +import CustomMarketDialog from '../components/CustomMarketDialog'; +import { notify } from '../utils/notifications'; -const { Option } = Select; +const { Option, OptGroup } = Select; const Wrapper = styled.div` height: 100%; @@ -28,9 +35,16 @@ const Wrapper = styled.div` `; export default function TradePage() { - const { marketName, market } = useMarket(); + const { + market, + marketName, + customMarkets, + setCustomMarkets, + setMarketAddress, + } = useMarket(); const markets = useMarketsList(); const [handleDeprecated, setHandleDeprecated] = useState(false); + const [addMarketVisible, setAddMarketVisible] = useState(false); const deprecatedMarkets = useUnmigratedDeprecatedMarkets(); const [dimensions, setDimensions] = useState({ height: window.innerHeight, @@ -83,8 +97,34 @@ export default function TradePage() { } }, [width, componentProps, handleDeprecated]); + const onAddCustomMarket = (customMarket) => { + const marketInfo = getMarketInfos(customMarkets).some( + (m) => m.address.toBase58() === customMarket.address, + ); + if (marketInfo) { + notify({ + message: `A market with the given ID already exists`, + type: 'error', + }); + return; + } + const newCustomMarkets = [...customMarkets, customMarket]; + setCustomMarkets(newCustomMarkets); + setMarketAddress(customMarket.address); + }; + + const onDeleteCustomMarket = (address) => { + const newCustomMarkets = customMarkets.filter((m) => m.address !== address); + setCustomMarkets(newCustomMarkets); + }; + return ( <> + setAddMarketVisible(false)} + onAddCustomMarket={onAddCustomMarket} + /> {market ? ( @@ -110,6 +152,12 @@ export default function TradePage() { ) : null} + + setAddMarketVisible(true)} + /> + {deprecatedMarkets && deprecatedMarkets.length > 0 && ( @@ -132,7 +180,13 @@ export default function TradePage() { ); } -function MarketSelector({ markets, placeholder, setHandleDeprecated }) { +function MarketSelector({ + markets, + placeholder, + setHandleDeprecated, + customMarkets, + onDeleteCustomMarket, +}) { const { market, setMarketAddress } = useMarket(); const onSetMarketAddress = (marketAddress) => { @@ -143,6 +197,13 @@ function MarketSelector({ markets, placeholder, setHandleDeprecated }) { const extractBase = (a) => a.split('/')[0]; const extractQuote = (a) => a.split('/')[1]; + const selectedMarket = getMarketInfos(customMarkets) + .find( + (proposedMarket) => + market?.address && proposedMarket.address.equals(market.address), + ) + ?.address?.toBase58(); + return ( ); } diff --git a/src/utils/markets.js b/src/utils/markets.js index 92e08ff..92bed36 100644 --- a/src/utils/markets.js +++ b/src/utils/markets.js @@ -48,7 +48,7 @@ export function useAllMarkets() { markets.push({ market, marketName: marketInfo.name }); } catch (e) { notify({ - message: 'Error loading market', + message: 'Error loading all market', description: e.message, type: 'error', }); @@ -143,23 +143,27 @@ export const DEFAULT_MARKET = USE_MARKETS.find( ({ name }) => name === 'SRM/USDT', ); -function getMarketDetails(market) { +function getMarketDetails(market, customMarkets) { if (!market) { return {}; } - const marketInfo = USE_MARKETS.find((otherMarket) => + const marketInfos = getMarketInfos(customMarkets); + const marketInfo = marketInfos.find((otherMarket) => otherMarket.address.equals(market.address), ); const baseCurrency = (market?.baseMintAddress && TOKEN_MINTS.find((token) => token.address.equals(market.baseMintAddress)) ?.name) || + (marketInfo?.baseLabel && `${marketInfo?.baseLabel}*`) || 'UNKNOWN'; const quoteCurrency = (market?.quoteMintAddress && TOKEN_MINTS.find((token) => token.address.equals(market.quoteMintAddress)) ?.name) || + (marketInfo?.quoteLabel && `${marketInfo?.quoteLabel}*`) || 'UNKNOWN'; + return { ...marketInfo, marketName: marketInfo?.name, @@ -174,9 +178,15 @@ export function MarketProvider({ children }) { 'marketAddress', DEFAULT_MARKET.address.toBase58(), ); + const [customMarkets, setCustomMarkets] = useLocalStorageState( + 'customMarkets', + [], + ); + const address = new PublicKey(marketAddress); const connection = useConnection(); - const marketInfo = USE_MARKETS.find((market) => + const marketInfos = getMarketInfos(customMarkets); + const marketInfo = marketInfos.find((market) => market.address.equals(address), ); @@ -191,6 +201,13 @@ export function MarketProvider({ children }) { const [market, setMarket] = useState(); useEffect(() => { + if ( + market && + marketInfo && + market._decoded.ownAddress?.equals(marketInfo?.address) + ) { + return; + } setMarket(null); if (!marketInfo || !marketInfo.address) { notify({ @@ -209,14 +226,17 @@ export function MarketProvider({ children }) { type: 'error', }), ); + // eslint-disable-next-line }, [connection, marketInfo]); return ( {children} @@ -996,3 +1016,13 @@ export function useBalancesForDeprecatedMarkets() { }); return openOrderAccountBalances; } + +export function getMarketInfos(customMarkets) { + const customMarketsInfo = customMarkets.map((m) => ({ + ...m, + address: new PublicKey(m.address), + programId: new PublicKey(m.programId), + })); + + return [...customMarketsInfo, ...USE_MARKETS]; +} diff --git a/src/utils/utils.js b/src/utils/utils.js index 3130602..2eff629 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,4 +1,17 @@ import { useCallback, useEffect, useState } from 'react'; +import { PublicKey } from '@solana/web3.js'; + +export function isValidPublicKey(key) { + if (!key) { + return false; + } + try { + new PublicKey(key); + return true; + } catch { + return false; + } +} export async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms));