Add page for initializing pools

This commit is contained in:
Gary Wang 2020-11-01 02:29:19 -08:00
parent 58faf7506a
commit c89d0c1281
11 changed files with 827 additions and 397 deletions

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
</>

View File

@ -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,

View File

@ -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...',

View File

@ -10,6 +10,7 @@
"noImplicitAny": false,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,

762
yarn.lock

File diff suppressed because it is too large Load Diff