Add page for initializing pools
This commit is contained in:
parent
58faf7506a
commit
c89d0c1281
|
@ -6,13 +6,15 @@
|
|||
"dependencies": {
|
||||
"@ant-design/icons": "^4.2.1",
|
||||
"@craco/craco": "^5.6.4",
|
||||
"@project-serum/serum": "0.13.10",
|
||||
"@project-serum/pool": "^0.1.1",
|
||||
"@project-serum/serum": "^0.13.10",
|
||||
"@project-serum/sol-wallet-adapter": "^0.1.1",
|
||||
"@solana/web3.js": "0.83.1",
|
||||
"@solana/web3.js": "0.86.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@types/bn.js": "^4.11.6",
|
||||
"@types/jest": "^26.0.14",
|
||||
"@types/node": "^14.11.4",
|
||||
"@types/react": "^16.9.51",
|
||||
|
@ -26,6 +28,7 @@
|
|||
"react-app-polyfill": "^1.0.5",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-is": "^17.0.1",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.3",
|
||||
|
|
|
@ -121,9 +121,10 @@ export default function OpenOrderTable({
|
|||
),
|
||||
},
|
||||
];
|
||||
const dataSource = (openOrders || []).map((order) =>
|
||||
Object.assign(order, { key: order.orderId }),
|
||||
);
|
||||
const dataSource = (openOrders || []).map((order) => ({
|
||||
...order,
|
||||
key: order.orderId,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Row>
|
||||
|
|
|
@ -7,6 +7,6 @@ const Wrapper = styled.div`
|
|||
background-color: #1a2029;
|
||||
`;
|
||||
|
||||
export default function FloatingElement({ style, children }) {
|
||||
export default function FloatingElement({ style = undefined, children }) {
|
||||
return <Wrapper style={{ ...style }}>{children}</Wrapper>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import React, { ReactElement, useMemo, useState } from 'react';
|
||||
import { useAccountInfo } from '../utils/connection';
|
||||
import { isValidPublicKey } from '../utils/utils';
|
||||
import { ValidateStatus } from 'antd/lib/form/FormItem';
|
||||
import { TokenInstructions } from '@project-serum/serum';
|
||||
import { parseTokenMintData } from '../utils/tokens';
|
||||
import { Form, Input, Tooltip } from 'antd';
|
||||
import Link from './Link';
|
||||
|
||||
export interface MintInfo {
|
||||
address: PublicKey;
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export function useMintInput(
|
||||
name,
|
||||
label: string | ReactElement,
|
||||
tooltip?: string | ReactElement,
|
||||
): [ReactElement, MintInfo | null] {
|
||||
const [address, setAddress] = useState('');
|
||||
const [accountInfo, loaded] = useAccountInfo(
|
||||
isValidPublicKey(address) ? new PublicKey(address) : null,
|
||||
);
|
||||
|
||||
const { validateStatus, hasFeedback, help, mintInfo } = useMemo(() => {
|
||||
let validateStatus: ValidateStatus = '';
|
||||
let hasFeedback = false;
|
||||
let help: string | null = null;
|
||||
let mintInfo: MintInfo | null = 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 = 'validating';
|
||||
} else {
|
||||
validateStatus = 'error';
|
||||
help = 'Invalid Solana address';
|
||||
}
|
||||
}
|
||||
return { validateStatus, hasFeedback, help, mintInfo };
|
||||
}, [address, accountInfo, loaded]);
|
||||
|
||||
const input = (
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
{tooltip} You can look up token mint addresses on{' '}
|
||||
<Link external to="https://sollet.io">
|
||||
sollet.io
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Tooltip>
|
||||
}
|
||||
name={name}
|
||||
validateStatus={validateStatus}
|
||||
hasFeedback={hasFeedback}
|
||||
help={help}
|
||||
>
|
||||
<Input
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value.trim())}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
return [input, mintInfo];
|
||||
}
|
|
@ -1,16 +1,13 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Form, Input, Tooltip, 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 { MARKETS } from '@project-serum/serum';
|
||||
import { 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';
|
||||
import Link from '../components/Link';
|
||||
import { useMintInput } from '../components/useMintInput';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
|
@ -195,75 +192,3 @@ export default function ListNewMarketPage() {
|
|||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function useMintInput(name, label, tooltip) {
|
||||
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={
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
{tooltip} You can look up token mint addresses on{' '}
|
||||
<Link external to="https://sollet.io">
|
||||
sollet.io
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Tooltip>
|
||||
}
|
||||
name={name}
|
||||
validateStatus={validateStatus}
|
||||
hasFeedback={hasFeedback}
|
||||
help={help}
|
||||
>
|
||||
<Input
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value.trim())}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
return [input, mintInfo];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Input, Tooltip, Typography } from 'antd';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { useConnection } from '../../utils/connection';
|
||||
import FloatingElement from '../../components/layout/FloatingElement';
|
||||
import styled from 'styled-components';
|
||||
import { useWallet } from '../../utils/wallet';
|
||||
import { sendSignedTransaction, signTransaction } from '../../utils/send';
|
||||
import { useMintInput } from '../../components/useMintInput';
|
||||
import { PoolTransactions } from '@project-serum/pool';
|
||||
import { useTokenAccounts } from '../../utils/markets';
|
||||
import BN from 'bn.js';
|
||||
import { notify } from '../../utils/notifications';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const AddRemoveTokenButtons = styled.div`
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const DEFAULT_PROGRAM_ID = '8qZoqDMXTfLZz6BYrDfD5Cuy65JKkaNwktb54hj1yaoK';
|
||||
|
||||
export default function NewPoolPage() {
|
||||
const connection = useConnection();
|
||||
const { wallet, connected } = useWallet();
|
||||
const [poolName, setPoolName] = useState('');
|
||||
const [programId, setProgramId] = useState(DEFAULT_PROGRAM_ID);
|
||||
const [initialSupply, setInitialSupply] = useState('1');
|
||||
const [initialAssets, setInitialAssets] = useState<InitialAsset[]>([
|
||||
{ valid: false },
|
||||
{ valid: false },
|
||||
]);
|
||||
const [tokenAccounts] = useTokenAccounts();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [newPoolAddress, setNewPoolAddress] = useState<PublicKey | null>(null);
|
||||
|
||||
const canSubmit =
|
||||
connected &&
|
||||
poolName.trim() &&
|
||||
programId &&
|
||||
parseFloat(initialSupply) > 0 &&
|
||||
initialAssets.every((asset) => asset.valid) &&
|
||||
tokenAccounts;
|
||||
|
||||
async function onSubmit() {
|
||||
if (!canSubmit) {
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const assets = initialAssets as ValidInitialAsset[];
|
||||
const [
|
||||
poolAddress,
|
||||
transactions,
|
||||
] = await PoolTransactions.initializeSimplePool({
|
||||
connection,
|
||||
programId: new PublicKey(programId),
|
||||
poolName,
|
||||
poolStateSpace: 1024,
|
||||
poolMintDecimals: 6,
|
||||
initialPoolMintSupply: new BN(
|
||||
Math.round(10 ** 6 * parseFloat(initialSupply)),
|
||||
),
|
||||
assetMints: assets.map((asset) => asset.mint),
|
||||
initialAssetQuantities: assets.map((asset) => new BN(asset.quantity)),
|
||||
creator: wallet.publicKey,
|
||||
creatorAssets: assets.map((asset) => {
|
||||
const found = tokenAccounts?.find((tokenAccount) =>
|
||||
tokenAccount.effectiveMint.equals(asset.mint),
|
||||
);
|
||||
if (!found) {
|
||||
throw new Error('No token account for ' + asset.mint.toBase58());
|
||||
}
|
||||
return found.pubkey;
|
||||
}),
|
||||
});
|
||||
const signed = await Promise.all(
|
||||
transactions.map(({ transaction, signers }) =>
|
||||
signTransaction({ transaction, wallet, signers, connection }),
|
||||
),
|
||||
);
|
||||
for (let signedTransaction of signed) {
|
||||
await sendSignedTransaction({ signedTransaction, connection });
|
||||
}
|
||||
setNewPoolAddress(poolAddress);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
notify({
|
||||
message: 'Error creating new pool: ' + e.message,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<FloatingElement>
|
||||
<Title level={4}>Create new pool</Title>
|
||||
<Form layout="vertical" onFinish={onSubmit}>
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Public name of the pool.">Pool Name</Tooltip>
|
||||
}
|
||||
name="name"
|
||||
>
|
||||
<Input
|
||||
value={poolName}
|
||||
onChange={(e) => setPoolName(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Address of the pool program.">
|
||||
Program ID{' '}
|
||||
<Text type="secondary">(e.g. {DEFAULT_PROGRAM_ID})</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
name="programId"
|
||||
initialValue={DEFAULT_PROGRAM_ID}
|
||||
>
|
||||
<Input
|
||||
value={programId}
|
||||
onChange={(e) => setProgramId(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Initial number of pool tokens to mint to your account.">
|
||||
Initial Pool Token Supply
|
||||
</Tooltip>
|
||||
}
|
||||
name="initialSupply"
|
||||
initialValue="1"
|
||||
>
|
||||
<Input
|
||||
value={initialSupply}
|
||||
onChange={(e) => setInitialSupply(e.target.value.trim())}
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
/>
|
||||
</Form.Item>
|
||||
<AddRemoveTokenButtons>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setInitialAssets((assets) => assets.concat({ valid: false }))
|
||||
}
|
||||
>
|
||||
Add token
|
||||
</Button>{' '}
|
||||
<Button
|
||||
onClick={() =>
|
||||
setInitialAssets((assets) => assets.slice(0, assets.length - 1))
|
||||
}
|
||||
disabled={initialAssets.length <= 1}
|
||||
>
|
||||
Remove token
|
||||
</Button>
|
||||
</AddRemoveTokenButtons>
|
||||
{initialAssets.map((asset, i) => (
|
||||
<AssetInput setInitialAssets={setInitialAssets} index={i} key={i} />
|
||||
))}
|
||||
<Form.Item label=" " colon={false}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={!canSubmit}
|
||||
loading={submitting}
|
||||
>
|
||||
{connected ? 'Submit' : 'Not connected to wallet'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</FloatingElement>
|
||||
{newPoolAddress ? (
|
||||
<FloatingElement>
|
||||
<Text>New pool address: {newPoolAddress.toBase58()}</Text>
|
||||
</FloatingElement>
|
||||
) : null}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
type InitialAsset = { valid: false } | ValidInitialAsset;
|
||||
interface ValidInitialAsset {
|
||||
valid: true;
|
||||
mint: PublicKey;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
function AssetInput({ setInitialAssets, index }) {
|
||||
const [mintInput, mintInfo] = useMintInput(
|
||||
`mint${index}`,
|
||||
<Text>Token {index + 1} Mint Address</Text>,
|
||||
<>Token mint address for token {index + 1}.</>,
|
||||
);
|
||||
const [quantity, setQuantity] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let change: InitialAsset;
|
||||
if (mintInfo && parseFloat(quantity) >= 0) {
|
||||
let parsedQuantity = Math.round(
|
||||
10 ** mintInfo.decimals * parseFloat(quantity),
|
||||
);
|
||||
change = {
|
||||
mint: mintInfo.address,
|
||||
quantity: parsedQuantity,
|
||||
valid: true,
|
||||
};
|
||||
} else {
|
||||
change = { valid: false };
|
||||
}
|
||||
setInitialAssets((assets: InitialAsset[]) =>
|
||||
assets.map((old, i) => (i === index ? change : old)),
|
||||
);
|
||||
}, [setInitialAssets, index, mintInfo, quantity]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mintInput}
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Initial quantity of token {index + 1} to deposit into the pool.
|
||||
</>
|
||||
}
|
||||
>
|
||||
Token {index + 1} Initial Quantity
|
||||
</Tooltip>
|
||||
}
|
||||
name={`quantity${index}`}
|
||||
validateStatus={'success'}
|
||||
>
|
||||
<Input
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(e.target.value.trim())}
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -6,6 +6,7 @@ import BalancesPage from './pages/BalancesPage';
|
|||
import ConvertPage from './pages/ConvertPage';
|
||||
import BasicLayout from './components/BasicLayout';
|
||||
import ListNewMarketPage from './pages/ListNewMarketPage';
|
||||
import NewPoolPage from './pages/pools/NewPoolPage';
|
||||
|
||||
export function Routes() {
|
||||
return (
|
||||
|
@ -17,6 +18,7 @@ export function Routes() {
|
|||
<Route exact path="/balances" component={BalancesPage} />
|
||||
<Route exact path="/convert" component={ConvertPage} />
|
||||
<Route exact path="/list-new-market" component={ListNewMarketPage} />
|
||||
<Route exact path="/pools/new" component={NewPoolPage} />
|
||||
</BasicLayout>
|
||||
</HashRouter>
|
||||
</>
|
||||
|
|
|
@ -20,7 +20,7 @@ import { useAccountData, useAccountInfo, useConnection } from './connection';
|
|||
import { useWallet } from './wallet';
|
||||
import tuple from 'immutable-tuple';
|
||||
import { notify } from './notifications';
|
||||
import { BN } from 'bn.js';
|
||||
import BN from 'bn.js';
|
||||
import {
|
||||
getTokenAccountInfo,
|
||||
parseTokenAccountData,
|
||||
|
|
|
@ -11,13 +11,13 @@ import {
|
|||
Transaction,
|
||||
TransactionSignature,
|
||||
} from '@solana/web3.js';
|
||||
import { BN } from 'bn.js';
|
||||
import BN from 'bn.js';
|
||||
import {
|
||||
DexInstructions,
|
||||
Market,
|
||||
OpenOrders,
|
||||
TOKEN_MINTS,
|
||||
TokenInstructions,
|
||||
OpenOrders,
|
||||
} from '@project-serum/serum';
|
||||
import Wallet from '@project-serum/sol-wallet-adapter';
|
||||
import { SelectedTokenAccounts, TokenAccount } from './types';
|
||||
|
@ -588,7 +588,7 @@ const getUnixTs = () => {
|
|||
|
||||
const DEFAULT_TIMEOUT = 15000;
|
||||
|
||||
async function sendTransaction({
|
||||
export async function sendTransaction({
|
||||
transaction,
|
||||
wallet,
|
||||
signers = [],
|
||||
|
@ -623,7 +623,7 @@ async function sendTransaction({
|
|||
});
|
||||
}
|
||||
|
||||
async function signTransaction({
|
||||
export async function signTransaction({
|
||||
transaction,
|
||||
wallet,
|
||||
signers = [],
|
||||
|
@ -644,7 +644,7 @@ async function signTransaction({
|
|||
return await wallet.signTransaction(transaction);
|
||||
}
|
||||
|
||||
async function sendSignedTransaction({
|
||||
export async function sendSignedTransaction({
|
||||
signedTransaction,
|
||||
connection,
|
||||
sendingMessage = 'Sending transaction...',
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"noImplicitAny": false,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
|
|
Loading…
Reference in New Issue