Add page for listing new markets
This commit is contained in:
parent
ce57e3c1e1
commit
37067f2f6c
|
@ -1,10 +1,10 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Input, Row, Col, Typography } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Col, Input, Modal, Row, 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 { useAccountInfo, useConnection } from '../utils/connection';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
@ -15,23 +15,27 @@ export default function CustomMarketDialog({
|
|||
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 [marketId, setMarketId] = useState('');
|
||||
|
||||
const [marketLabel, setMarketLabel] = useState('');
|
||||
const [baseLabel, setBaseLabel] = useState('');
|
||||
const [quoteLabel, setQuoteLabel] = useState('');
|
||||
|
||||
const [market, setMarket] = useState(null);
|
||||
const [loadingMarket, setLoadingMarket] = useState(false);
|
||||
|
||||
const wellFormedMarketId = isValidPublicKey(marketId);
|
||||
const wellFormedProgramId = isValidPublicKey(programId);
|
||||
|
||||
const [marketAccountInfo] = useAccountInfo(
|
||||
wellFormedMarketId ? new PublicKey(marketId) : null,
|
||||
);
|
||||
const programId = marketAccountInfo
|
||||
? marketAccountInfo.owner.toBase58()
|
||||
: MARKETS.find(({ deprecated }) => !deprecated).programId.toBase58();
|
||||
|
||||
useEffect(() => {
|
||||
if (!wellFormedMarketId || !wellFormedProgramId) {
|
||||
if (!wellFormedMarketId || !programId) {
|
||||
resetLabels();
|
||||
return;
|
||||
}
|
||||
|
@ -79,12 +83,12 @@ export default function CustomMarketDialog({
|
|||
const canSubmit =
|
||||
!loadingMarket &&
|
||||
!!market &&
|
||||
market.publicKey.toBase58() === marketId &&
|
||||
marketId &&
|
||||
programId &&
|
||||
marketLabel &&
|
||||
(knownBaseCurrency || baseLabel) &&
|
||||
(knownQuoteCurrency || quoteLabel) &&
|
||||
wellFormedProgramId &&
|
||||
wellFormedMarketId;
|
||||
|
||||
const onSubmit = () => {
|
||||
|
@ -115,7 +119,6 @@ export default function CustomMarketDialog({
|
|||
resetLabels();
|
||||
setMarket(null);
|
||||
setMarketId(null);
|
||||
setProgramId(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
@ -129,13 +132,11 @@ export default function CustomMarketDialog({
|
|||
okButtonProps={{ disabled: !canSubmit }}
|
||||
>
|
||||
<div>
|
||||
{wellFormedMarketId && wellFormedProgramId ? (
|
||||
{wellFormedMarketId ? (
|
||||
<>
|
||||
{!market && (
|
||||
{!market && !loadingMarket && (
|
||||
<Row style={{ marginBottom: 8 }}>
|
||||
<Text type="danger">
|
||||
Not a valid market and program ID combination
|
||||
</Text>
|
||||
<Text type="danger">Not a valid market</Text>
|
||||
</Row>
|
||||
)}
|
||||
{market && knownMarket && (
|
||||
|
@ -161,11 +162,6 @@ export default function CustomMarketDialog({
|
|||
<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 }}>
|
||||
|
@ -179,20 +175,6 @@ export default function CustomMarketDialog({
|
|||
</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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { InfoCircleOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Button, Menu, Popover, Select } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import logo from '../assets/logo.svg';
|
||||
import styled from 'styled-components';
|
||||
|
@ -29,7 +29,6 @@ const LogoWrapper = styled.div`
|
|||
`;
|
||||
|
||||
export default function TopBar() {
|
||||
const [current, setCurrent] = useState('/');
|
||||
const { connected, wallet, providerUrl, setProvider } = useWallet();
|
||||
const { endpoint, setEndpoint } = useConnectionConfig();
|
||||
const location = useLocation();
|
||||
|
@ -44,24 +43,16 @@ export default function TopBar() {
|
|||
[history],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname.includes('/orders')) {
|
||||
setCurrent('/orders');
|
||||
} else if (location.pathname.includes('/balances')) {
|
||||
setCurrent('/balances');
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<LogoWrapper>
|
||||
<img src={logo} alt="" />
|
||||
<img src={logo} alt="" onClick={() => history.push('/')} />
|
||||
{'SERUM'}
|
||||
</LogoWrapper>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
onClick={handleClick}
|
||||
selectedKeys={[current]}
|
||||
selectedKeys={[location.pathname]}
|
||||
style={{
|
||||
borderBottom: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Form, Input, Typography } from 'antd';
|
||||
import { notify } from '../utils/notifications';
|
||||
import { isValidPublicKey } from '../utils/utils';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { MARKETS, TokenInstructions } from '@project-serum/serum';
|
||||
import { useAccountInfo, useConnection } from '../utils/connection';
|
||||
import FloatingElement from '../components/layout/FloatingElement';
|
||||
import styled from 'styled-components';
|
||||
import { parseTokenMintData } from '../utils/tokens';
|
||||
import { useWallet } from '../utils/wallet';
|
||||
import { listMarket } from '../utils/send';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
export default function ListNewMarketPage() {
|
||||
const connection = useConnection();
|
||||
const { wallet, connected } = useWallet();
|
||||
const [baseMintInput, baseMintInfo] = useMintInput(
|
||||
'baseMint',
|
||||
'Base Mint Address',
|
||||
);
|
||||
const [quoteMintInput, quoteMintInfo] = useMintInput(
|
||||
'quoteMint',
|
||||
'Quote Mint Address',
|
||||
);
|
||||
const [lotSize, setLotSize] = useState('1');
|
||||
const [tickSize, setTickSize] = useState('0.01');
|
||||
const dexProgramId = MARKETS.find(({ deprecated }) => !deprecated).programId;
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const [listedMarket, setListedMarket] = useState(null);
|
||||
|
||||
let baseLotSize;
|
||||
let quoteLotSize;
|
||||
if (baseMintInfo && parseFloat(lotSize) > 0) {
|
||||
baseLotSize = Math.round(10 ** baseMintInfo.decimals * parseFloat(lotSize));
|
||||
if (quoteMintInfo && parseFloat(tickSize) > 0) {
|
||||
quoteLotSize = Math.round(
|
||||
parseFloat(lotSize) *
|
||||
10 ** quoteMintInfo.decimals *
|
||||
parseFloat(tickSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const canSubmit =
|
||||
connected &&
|
||||
!!baseMintInfo &&
|
||||
!!quoteMintInfo &&
|
||||
!!baseLotSize &&
|
||||
!!quoteLotSize;
|
||||
|
||||
async function onSubmit() {
|
||||
if (!canSubmit) {
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const marketAddress = await listMarket({
|
||||
connection,
|
||||
wallet,
|
||||
baseMint: baseMintInfo.address,
|
||||
quoteMint: quoteMintInfo.address,
|
||||
baseLotSize,
|
||||
quoteLotSize,
|
||||
dexProgramId,
|
||||
});
|
||||
setListedMarket(marketAddress);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
notify({
|
||||
message: 'Error listing new market: ' + e.message,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<FloatingElement>
|
||||
<Title level={4}>List New Market</Title>
|
||||
<Form
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
{baseMintInput}
|
||||
{quoteMintInput}
|
||||
<Form.Item
|
||||
label="Min Order Size (Lot Size)"
|
||||
name="lotSize"
|
||||
initialValue="1"
|
||||
validateStatus={
|
||||
baseMintInfo && quoteMintInfo
|
||||
? baseLotSize
|
||||
? 'success'
|
||||
: 'error'
|
||||
: null
|
||||
}
|
||||
hasFeedback={baseMintInfo && quoteMintInfo}
|
||||
>
|
||||
<Input
|
||||
value={lotSize}
|
||||
onChange={(e) => setLotSize(e.target.value.trim())}
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Tick Size (Price Increment)"
|
||||
name="tickSize"
|
||||
initialValue="0.01"
|
||||
validateStatus={
|
||||
baseMintInfo && quoteMintInfo
|
||||
? quoteLotSize
|
||||
? 'success'
|
||||
: 'error'
|
||||
: null
|
||||
}
|
||||
hasFeedback={baseMintInfo && quoteMintInfo}
|
||||
>
|
||||
<Input
|
||||
value={tickSize}
|
||||
onChange={(e) => setTickSize(e.target.value.trim())}
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={!canSubmit}
|
||||
loading={submitting}
|
||||
>
|
||||
{connected ? 'Submit' : 'Not connected to wallet'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</FloatingElement>
|
||||
{listedMarket ? (
|
||||
<FloatingElement>
|
||||
<Text>New market address: {listedMarket.toBase58()}</Text>
|
||||
</FloatingElement>
|
||||
) : null}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function useMintInput(name, label) {
|
||||
const [address, setAddress] = useState('');
|
||||
const [accountInfo, loaded] = useAccountInfo(
|
||||
isValidPublicKey(address) ? new PublicKey(address) : null,
|
||||
);
|
||||
|
||||
let validateStatus = null;
|
||||
let hasFeedback = false;
|
||||
let help = null;
|
||||
let mintInfo = null;
|
||||
if (address) {
|
||||
hasFeedback = true;
|
||||
if (accountInfo) {
|
||||
if (
|
||||
accountInfo.owner.equals(TokenInstructions.TOKEN_PROGRAM_ID) &&
|
||||
accountInfo.data.length === 82
|
||||
) {
|
||||
let parsed = parseTokenMintData(accountInfo.data);
|
||||
if (parsed.initialized) {
|
||||
validateStatus = 'success';
|
||||
mintInfo = {
|
||||
address: new PublicKey(address),
|
||||
decimals: parsed.decimals,
|
||||
};
|
||||
} else {
|
||||
validateStatus = 'error';
|
||||
help = 'Invalid SPL mint';
|
||||
}
|
||||
} else {
|
||||
validateStatus = 'error';
|
||||
help = 'Invalid SPL mint address';
|
||||
}
|
||||
} else if (isValidPublicKey(address) && !loaded) {
|
||||
validateStatus = 'loading';
|
||||
} else {
|
||||
validateStatus = 'error';
|
||||
help = 'Invalid Solana address';
|
||||
}
|
||||
}
|
||||
|
||||
const input = (
|
||||
<Form.Item
|
||||
label={label}
|
||||
name={name}
|
||||
validateStatus={validateStatus}
|
||||
hasFeedback={hasFeedback}
|
||||
help={help}
|
||||
>
|
||||
<Input
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value.trim())}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
return [input, mintInfo];
|
||||
}
|
|
@ -4,6 +4,7 @@ import OpenOrdersPage from './pages/OpenOrdersPage';
|
|||
import React from 'react';
|
||||
import BalancesPage from './pages/BalancesPage';
|
||||
import BasicLayout from './components/BasicLayout';
|
||||
import ListNewMarketPage from './pages/ListNewMarketPage';
|
||||
|
||||
export function Routes() {
|
||||
return (
|
||||
|
@ -11,6 +12,11 @@ export function Routes() {
|
|||
<Route exact path="/" component={TradePageContents} />
|
||||
<Route exact path="/orders" component={OpenOrdersPageContents} />
|
||||
<Route exact path="/balances" component={BalancesPageContents} />
|
||||
<Route exact path="/list-new-market">
|
||||
<BasicLayout>
|
||||
<ListNewMarketPage />
|
||||
</BasicLayout>
|
||||
</Route>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,17 @@ import { notify } from './notifications';
|
|||
import { getDecimalCount, sleep } from './utils';
|
||||
import {
|
||||
Account,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
PublicKey,
|
||||
} from '@solana/web3.js';
|
||||
import { TOKEN_MINTS, TokenInstructions } from '@project-serum/serum';
|
||||
import { BN } from 'bn.js';
|
||||
import {
|
||||
DexInstructions,
|
||||
Market,
|
||||
TOKEN_MINTS,
|
||||
TokenInstructions,
|
||||
} from '@project-serum/serum';
|
||||
|
||||
export async function createTokenAccountTransaction({
|
||||
connection,
|
||||
|
@ -249,6 +255,145 @@ export async function placeOrder({
|
|||
});
|
||||
}
|
||||
|
||||
export async function listMarket({
|
||||
connection,
|
||||
wallet,
|
||||
baseMint,
|
||||
quoteMint,
|
||||
baseLotSize,
|
||||
quoteLotSize,
|
||||
dexProgramId,
|
||||
}) {
|
||||
const market = new Account();
|
||||
const requestQueue = new Account();
|
||||
const eventQueue = new Account();
|
||||
const bids = new Account();
|
||||
const asks = new Account();
|
||||
const baseVault = new Account();
|
||||
const quoteVault = new Account();
|
||||
const feeRateBps = 0;
|
||||
const quoteDustThreshold = new BN(100);
|
||||
|
||||
async function getVaultOwnerAndNonce() {
|
||||
const nonce = new BN(0);
|
||||
while (true) {
|
||||
try {
|
||||
const vaultOwner = await PublicKey.createProgramAddress(
|
||||
[market.publicKey.toBuffer(), nonce.toArrayLike(Buffer, 'le', 8)],
|
||||
dexProgramId,
|
||||
);
|
||||
return [vaultOwner, nonce];
|
||||
} catch (e) {
|
||||
nonce.iaddn(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce();
|
||||
|
||||
const tx1 = new Transaction();
|
||||
tx1.add(
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: baseVault.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(165),
|
||||
space: 165,
|
||||
programId: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
}),
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: quoteVault.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(165),
|
||||
space: 165,
|
||||
programId: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
}),
|
||||
TokenInstructions.initializeAccount({
|
||||
account: baseVault.publicKey,
|
||||
mint: baseMint,
|
||||
owner: vaultOwner,
|
||||
}),
|
||||
TokenInstructions.initializeAccount({
|
||||
account: quoteVault.publicKey,
|
||||
mint: quoteMint,
|
||||
owner: vaultOwner,
|
||||
}),
|
||||
);
|
||||
|
||||
const tx2 = new Transaction();
|
||||
tx2.add(
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: market.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(
|
||||
Market.getLayout(dexProgramId).span,
|
||||
),
|
||||
space: Market.getLayout(dexProgramId).span,
|
||||
programId: dexProgramId,
|
||||
}),
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: requestQueue.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
|
||||
space: 5120 + 12,
|
||||
programId: dexProgramId,
|
||||
}),
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: eventQueue.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
|
||||
space: 262144 + 12,
|
||||
programId: dexProgramId,
|
||||
}),
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: bids.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
|
||||
space: 65536 + 12,
|
||||
programId: dexProgramId,
|
||||
}),
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: asks.publicKey,
|
||||
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
|
||||
space: 65536 + 12,
|
||||
programId: dexProgramId,
|
||||
}),
|
||||
DexInstructions.initializeMarket({
|
||||
market: market.publicKey,
|
||||
requestQueue: requestQueue.publicKey,
|
||||
eventQueue: eventQueue.publicKey,
|
||||
bids: bids.publicKey,
|
||||
asks: asks.publicKey,
|
||||
baseVault: baseVault.publicKey,
|
||||
quoteVault: quoteVault.publicKey,
|
||||
baseMint,
|
||||
quoteMint,
|
||||
baseLotSize: new BN(baseLotSize),
|
||||
quoteLotSize: new BN(quoteLotSize),
|
||||
feeRateBps,
|
||||
vaultSignerNonce,
|
||||
quoteDustThreshold,
|
||||
programId: dexProgramId,
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
sendTransaction({
|
||||
transaction: tx1,
|
||||
wallet,
|
||||
connection,
|
||||
signers: [wallet.publicKey, baseVault, quoteVault],
|
||||
}),
|
||||
sendTransaction({
|
||||
transaction: tx2,
|
||||
wallet,
|
||||
connection,
|
||||
signers: [wallet.publicKey, market, requestQueue, eventQueue, bids, asks],
|
||||
}),
|
||||
]);
|
||||
|
||||
return market.publicKey;
|
||||
}
|
||||
|
||||
const getUnixTs = () => {
|
||||
return new Date().getTime() / 1000;
|
||||
};
|
||||
|
|
|
@ -10,6 +10,13 @@ export const ACCOUNT_LAYOUT = BufferLayout.struct([
|
|||
BufferLayout.blob(93),
|
||||
]);
|
||||
|
||||
export const MINT_LAYOUT = BufferLayout.struct([
|
||||
BufferLayout.blob(44),
|
||||
BufferLayout.u8('decimals'),
|
||||
BufferLayout.u8('initialized'),
|
||||
BufferLayout.blob(36),
|
||||
]);
|
||||
|
||||
export function parseTokenAccountData(data) {
|
||||
let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data);
|
||||
return {
|
||||
|
@ -19,6 +26,11 @@ export function parseTokenAccountData(data) {
|
|||
};
|
||||
}
|
||||
|
||||
export function parseTokenMintData(data) {
|
||||
let { decimals, initialized } = MINT_LAYOUT.decode(data);
|
||||
return { decimals, initialized };
|
||||
}
|
||||
|
||||
export function getOwnedAccountsFilters(publicKey) {
|
||||
return [
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue