345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
AutoComplete,
|
|
Button,
|
|
Form,
|
|
Input,
|
|
Switch,
|
|
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 { BaseSignerWalletAdapter } from '@solana/wallet-adapter-base';
|
|
import { useWallet } from '@solana/wallet-adapter-react';
|
|
import { sendSignedTransaction, signTransactions } 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';
|
|
import Link from '../../components/Link';
|
|
|
|
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 SIMPLE_POOL_PROGRAM_ID = '71JS8f7y7ASMbuuSMCVG7a3qDdcVco2qYD6bMJeZqUCm';
|
|
const ADMIN_CONTROLLED_POOL_PROGRAM_ID =
|
|
'WvmTNLpGMVbwJVYztYL4Hnsy82cJhQorxjnnXcRm3b6';
|
|
const DEFAULT_PROGRAM_ID = ADMIN_CONTROLLED_POOL_PROGRAM_ID;
|
|
const PROGRAM_ID_OPTIONS = [
|
|
{
|
|
label: `Simple Pool (${SIMPLE_POOL_PROGRAM_ID})`,
|
|
value: SIMPLE_POOL_PROGRAM_ID,
|
|
},
|
|
{
|
|
label: `Admin-Controlled Pool (${ADMIN_CONTROLLED_POOL_PROGRAM_ID})`,
|
|
value: ADMIN_CONTROLLED_POOL_PROGRAM_ID,
|
|
},
|
|
];
|
|
|
|
export default function NewPoolPage() {
|
|
const connection = useConnection();
|
|
const { connected, publicKey, wallet } = 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 [adminControlled, setAdminControlled] = useState(false);
|
|
const [adminAddress, setAdminAddress] = useState('');
|
|
const [tokenAccounts] = useTokenAccounts();
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [newPoolAddress, setNewPoolAddress] = useState<PublicKey | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (programId === SIMPLE_POOL_PROGRAM_ID) {
|
|
setAdminControlled(false);
|
|
} else if (programId === ADMIN_CONTROLLED_POOL_PROGRAM_ID) {
|
|
setAdminControlled(true);
|
|
}
|
|
}, [programId]);
|
|
|
|
useEffect(() => {
|
|
if (connected && publicKey) {
|
|
setAdminAddress(publicKey.toBase58());
|
|
}
|
|
}, [publicKey, connected]);
|
|
|
|
const canSubmit =
|
|
connected &&
|
|
poolName.trim() &&
|
|
programId &&
|
|
parseFloat(initialSupply) > 0 &&
|
|
initialAssets.every((asset) => asset.valid) &&
|
|
tokenAccounts &&
|
|
(adminAddress || !adminControlled);
|
|
|
|
async function onSubmit() {
|
|
if (!canSubmit || !publicKey || !wallet) {
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
const assets = initialAssets as ValidInitialAsset[];
|
|
const [
|
|
poolAddress,
|
|
transactionsAndSigners,
|
|
] = 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: 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;
|
|
}),
|
|
additionalAccounts: adminControlled
|
|
? [
|
|
{
|
|
pubkey: new PublicKey(adminAddress),
|
|
isSigner: false,
|
|
isWritable: false,
|
|
},
|
|
]
|
|
: [],
|
|
});
|
|
const signed = await signTransactions({
|
|
transactionsAndSigners,
|
|
wallet: wallet.adapter as BaseSignerWalletAdapter,
|
|
connection,
|
|
});
|
|
for (let signedTransaction of signed) {
|
|
await sendSignedTransaction({ signedTransaction, connection });
|
|
}
|
|
setNewPoolAddress(poolAddress);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
notify({
|
|
message: 'Error creating new pool',
|
|
description: 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}
|
|
>
|
|
<AutoComplete
|
|
value={programId}
|
|
onChange={(value) => setProgramId(value)}
|
|
options={PROGRAM_ID_OPTIONS}
|
|
/>
|
|
</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={
|
|
<Tooltip title="Whether the assets in the pool can be controlled by the pool admin.">
|
|
Admin Controlled
|
|
</Tooltip>
|
|
}
|
|
name="adminControlled"
|
|
>
|
|
<Switch
|
|
checked={adminControlled}
|
|
onChange={(checked) => setAdminControlled(checked)}
|
|
disabled={
|
|
programId === SIMPLE_POOL_PROGRAM_ID ||
|
|
programId === ADMIN_CONTROLLED_POOL_PROGRAM_ID
|
|
}
|
|
/>
|
|
</Form.Item>
|
|
{adminControlled ? (
|
|
<Form.Item
|
|
label={
|
|
<Tooltip title="Address of the pool admin account.">
|
|
Admin Address
|
|
</Tooltip>
|
|
}
|
|
>
|
|
<Input
|
|
value={adminAddress}
|
|
onChange={(e) => setAdminAddress(e.target.value.trim())}
|
|
/>
|
|
</Form.Item>
|
|
) : null}
|
|
<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:{' '}
|
|
<Link to={`/pools/${newPoolAddress.toBase58()}`}>
|
|
{newPoolAddress.toBase58()}
|
|
</Link>
|
|
</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>
|
|
</>
|
|
);
|
|
}
|