Custom markets input (#15)

Co-authored-by: Nishad <nishadsingh@gmail.com>
This commit is contained in:
philippe-ftx 2020-09-24 13:13:23 +02:00 committed by GitHub
parent b97cffe3c3
commit 40bc4f7d78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 412 additions and 44 deletions

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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];
}

View File

@ -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));