Custom markets input (#15)
Co-authored-by: Nishad <nishadsingh@gmail.com>
This commit is contained in:
parent
b97cffe3c3
commit
40bc4f7d78
|
@ -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 (
|
||||
<Modal
|
||||
title={'Add custom market'}
|
||||
visible={visible}
|
||||
onOk={onSubmit}
|
||||
okText={'Add'}
|
||||
onCancel={onDoClose}
|
||||
okButtonProps={{ disabled: !canSubmit }}
|
||||
>
|
||||
<div>
|
||||
{wellFormedMarketId && wellFormedProgramId ? (
|
||||
<>
|
||||
{!market && (
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Text type="danger">
|
||||
Not a valid market and program ID combination
|
||||
</Text>
|
||||
</Row>
|
||||
)}
|
||||
{market && knownMarket && (
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Text type="warning">Market known: {knownMarket.name}</Text>
|
||||
</Row>
|
||||
)}
|
||||
{market && !knownProgram && (
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Text type="danger">Warning: unknown DEX program</Text>
|
||||
</Row>
|
||||
)}
|
||||
{market && knownProgram && knownProgram.deprecated && (
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Text type="warning">Warning: deprecated DEX program</Text>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{marketId && !wellFormedMarketId && (
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Text type="danger">Invalid market ID</Text>
|
||||
</Row>
|
||||
)}
|
||||
{marketId && !wellFormedProgramId && (
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Text type="danger">Invalid program ID</Text>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Col span={24}>
|
||||
<Input
|
||||
placeholder="Market Id"
|
||||
value={marketId}
|
||||
onChange={(e) => setMarketId(e.target.value)}
|
||||
suffix={loadingMarket ? <LoadingOutlined /> : null}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{programId && !isValidPublicKey(programId) && (
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Text type="danger">Invalid program ID</Text>
|
||||
</Row>
|
||||
)}
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Col span={24}>
|
||||
<Input
|
||||
placeholder="Program Id"
|
||||
value={programId}
|
||||
onChange={(e) => setProgramId(e.target.value)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{ marginBottom: 8, marginTop: 8 }}>
|
||||
<Col span={24}>
|
||||
<Input
|
||||
placeholder="Market Label"
|
||||
disabled={!market}
|
||||
value={marketLabel}
|
||||
onChange={(e) => setMarketLabel(e.target.value)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[8]} style={{ marginBottom: 8 }}>
|
||||
<Col span={12}>
|
||||
<Input
|
||||
placeholder="Base label"
|
||||
disabled={!market || knownBaseCurrency}
|
||||
value={knownBaseCurrency || baseLabel}
|
||||
onChange={(e) => setBaseLabel(e.target.value)}
|
||||
/>
|
||||
{market && !knownBaseCurrency && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="warning">Warning: unknown token</Text>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Input
|
||||
placeholder="Quote label"
|
||||
disabled={!market || knownQuoteCurrency}
|
||||
value={knownQuoteCurrency || quoteLabel}
|
||||
onChange={(e) => setQuoteLabel(e.target.value)}
|
||||
/>
|
||||
{market && !knownQuoteCurrency && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="warning">Warning: unknown token</Text>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<>
|
||||
<CustomMarketDialog
|
||||
visible={addMarketVisible}
|
||||
onClose={() => setAddMarketVisible(false)}
|
||||
onAddCustomMarket={onAddCustomMarket}
|
||||
/>
|
||||
<Wrapper>
|
||||
<Row
|
||||
align="middle"
|
||||
|
@ -96,6 +136,8 @@ export default function TradePage() {
|
|||
markets={markets}
|
||||
setHandleDeprecated={setHandleDeprecated}
|
||||
placeholder={'Select market'}
|
||||
customMarkets={customMarkets}
|
||||
onDeleteCustomMarket={onDeleteCustomMarket}
|
||||
/>
|
||||
</Col>
|
||||
{market ? (
|
||||
|
@ -110,6 +152,12 @@ export default function TradePage() {
|
|||
</Popover>
|
||||
</Col>
|
||||
) : null}
|
||||
<Col>
|
||||
<PlusCircleOutlined
|
||||
style={{ color: '#2abdd2' }}
|
||||
onClick={() => setAddMarketVisible(true)}
|
||||
/>
|
||||
</Col>
|
||||
{deprecatedMarkets && deprecatedMarkets.length > 0 && (
|
||||
<React.Fragment>
|
||||
<Col>
|
||||
|
@ -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 (
|
||||
<Select
|
||||
showSearch
|
||||
|
@ -152,45 +213,72 @@ function MarketSelector({ markets, placeholder, setHandleDeprecated }) {
|
|||
optionFilterProp="name"
|
||||
onSelect={onSetMarketAddress}
|
||||
listHeight={400}
|
||||
value={markets
|
||||
.find(
|
||||
(proposedMarket) =>
|
||||
market?.address && proposedMarket.address.equals(market.address),
|
||||
)
|
||||
?.address?.toBase58()}
|
||||
value={selectedMarket}
|
||||
filterOption={(input, option) =>
|
||||
option.name.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{markets
|
||||
.sort((a, b) =>
|
||||
extractQuote(a.name) === 'USDT' && extractQuote(b.name) !== 'USDT'
|
||||
? -1
|
||||
: extractQuote(a.name) !== 'USDT' && extractQuote(b.name) === 'USDT'
|
||||
? 1
|
||||
: 0,
|
||||
)
|
||||
.sort((a, b) =>
|
||||
extractBase(a.name) < extractBase(b.name)
|
||||
? -1
|
||||
: extractBase(a.name) > extractBase(b.name)
|
||||
? 1
|
||||
: 0,
|
||||
)
|
||||
.map(({ address, name, deprecated }, i) => (
|
||||
<Option
|
||||
value={address.toBase58()}
|
||||
key={address}
|
||||
name={name}
|
||||
style={{
|
||||
padding: '10px 0',
|
||||
textAlign: 'center',
|
||||
backgroundColor: i % 2 === 0 ? 'rgb(39, 44, 61)' : null,
|
||||
}}
|
||||
>
|
||||
{name} {deprecated ? ' (Deprecated)' : null}
|
||||
</Option>
|
||||
))}
|
||||
{customMarkets && customMarkets.length > 0 && (
|
||||
<OptGroup label="Custom">
|
||||
{customMarkets.map(({ address, name }, i) => (
|
||||
<Option
|
||||
value={address}
|
||||
key={address}
|
||||
name={name}
|
||||
style={{
|
||||
padding: '10px',
|
||||
backgroundColor: i % 2 === 0 ? 'rgb(39, 44, 61)' : null,
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
<Col flex="auto">{name}</Col>
|
||||
{selectedMarket !== address && (
|
||||
<Col>
|
||||
<DeleteOutlined
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
onDeleteCustomMarket && onDeleteCustomMarket(address);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
)}
|
||||
<OptGroup label="Markets">
|
||||
{markets
|
||||
.sort((a, b) =>
|
||||
extractQuote(a.name) === 'USDT' && extractQuote(b.name) !== 'USDT'
|
||||
? -1
|
||||
: extractQuote(a.name) !== 'USDT' &&
|
||||
extractQuote(b.name) === 'USDT'
|
||||
? 1
|
||||
: 0,
|
||||
)
|
||||
.sort((a, b) =>
|
||||
extractBase(a.name) < extractBase(b.name)
|
||||
? -1
|
||||
: extractBase(a.name) > extractBase(b.name)
|
||||
? 1
|
||||
: 0,
|
||||
)
|
||||
.map(({ address, name, deprecated }, i) => (
|
||||
<Option
|
||||
value={address.toBase58()}
|
||||
key={address}
|
||||
name={name}
|
||||
style={{
|
||||
padding: '10px',
|
||||
backgroundColor: i % 2 === 0 ? 'rgb(39, 44, 61)' : null,
|
||||
}}
|
||||
>
|
||||
{name} {deprecated ? ' (Deprecated)' : null}
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<MarketContext.Provider
|
||||
value={{
|
||||
market,
|
||||
...getMarketDetails(market),
|
||||
...getMarketDetails(market, customMarkets),
|
||||
setMarketAddress,
|
||||
customMarkets,
|
||||
setCustomMarkets,
|
||||
}}
|
||||
>
|
||||
{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];
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in New Issue