Add support for admin-controlled pools
This commit is contained in:
parent
f7057035fc
commit
42fec272c9
|
@ -6,8 +6,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.2.1",
|
"@ant-design/icons": "^4.2.1",
|
||||||
"@craco/craco": "^5.6.4",
|
"@craco/craco": "^5.6.4",
|
||||||
|
"@project-serum/associated-token": "0.1.0",
|
||||||
"@project-serum/awesome-serum": "1.0.1",
|
"@project-serum/awesome-serum": "1.0.1",
|
||||||
"@project-serum/pool": "^0.1.1",
|
"@project-serum/pool": "^0.2.0",
|
||||||
"@project-serum/serum": "^0.13.14",
|
"@project-serum/serum": "^0.13.14",
|
||||||
"@project-serum/sol-wallet-adapter": "^0.1.1",
|
"@project-serum/sol-wallet-adapter": "^0.1.1",
|
||||||
"@solana/web3.js": "0.86.1",
|
"@solana/web3.js": "0.86.1",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Input, Tooltip, Typography } from 'antd';
|
import { Button, Form, Input, Switch, Tooltip, Typography } from 'antd';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import { useConnection } from '../../utils/connection';
|
import { useConnection } from '../../utils/connection';
|
||||||
import FloatingElement from '../../components/layout/FloatingElement';
|
import FloatingElement from '../../components/layout/FloatingElement';
|
||||||
|
@ -28,7 +28,7 @@ const AddRemoveTokenButtons = styled.div`
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DEFAULT_PROGRAM_ID = '8qZoqDMXTfLZz6BYrDfD5Cuy65JKkaNwktb54hj1yaoK';
|
const DEFAULT_PROGRAM_ID = 'DL7L4cFHwmfNevZRg92rF5unbdUvFGoiuMrQ4aV7Nzsc';
|
||||||
|
|
||||||
export default function NewPoolPage() {
|
export default function NewPoolPage() {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
|
@ -40,6 +40,7 @@ export default function NewPoolPage() {
|
||||||
{ valid: false },
|
{ valid: false },
|
||||||
{ valid: false },
|
{ valid: false },
|
||||||
]);
|
]);
|
||||||
|
const [adminControlled, setAdminControlled] = useState(false);
|
||||||
const [tokenAccounts] = useTokenAccounts();
|
const [tokenAccounts] = useTokenAccounts();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [newPoolAddress, setNewPoolAddress] = useState<PublicKey | null>(null);
|
const [newPoolAddress, setNewPoolAddress] = useState<PublicKey | null>(null);
|
||||||
|
@ -83,6 +84,9 @@ export default function NewPoolPage() {
|
||||||
}
|
}
|
||||||
return found.pubkey;
|
return found.pubkey;
|
||||||
}),
|
}),
|
||||||
|
additionalAccounts: adminControlled
|
||||||
|
? [{ pubkey: wallet.publicKey, isSigner: false, isWritable: false }]
|
||||||
|
: [],
|
||||||
});
|
});
|
||||||
const signed = await Promise.all(
|
const signed = await Promise.all(
|
||||||
transactions.map(({ transaction, signers }) =>
|
transactions.map(({ transaction, signers }) =>
|
||||||
|
@ -172,6 +176,19 @@ export default function NewPoolPage() {
|
||||||
{initialAssets.map((asset, i) => (
|
{initialAssets.map((asset, i) => (
|
||||||
<AssetInput setInitialAssets={setInitialAssets} index={i} key={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)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label=" " colon={false}>
|
<Form.Item label=" " colon={false}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
|
@ -0,0 +1,421 @@
|
||||||
|
import React, { FormEvent, useState } from 'react';
|
||||||
|
import { AdminControlledPoolInstructions, PoolInfo } from '@project-serum/pool';
|
||||||
|
import { TokenInstructions } from '@project-serum/serum';
|
||||||
|
import FloatingElement from '../../../components/layout/FloatingElement';
|
||||||
|
import { useConnection } from '../../../utils/connection';
|
||||||
|
import { useWallet } from '../../../utils/wallet';
|
||||||
|
import {
|
||||||
|
getSelectedTokenAccountForMint,
|
||||||
|
useTokenAccounts,
|
||||||
|
} from '../../../utils/markets';
|
||||||
|
import { sendTransaction } from '../../../utils/send';
|
||||||
|
import { notify } from '../../../utils/notifications';
|
||||||
|
import { PublicKey, Transaction } from '@solana/web3.js';
|
||||||
|
import { Button, Input, Tabs } from 'antd';
|
||||||
|
import {
|
||||||
|
createAssociatedTokenAccount,
|
||||||
|
getAssociatedTokenAddress,
|
||||||
|
} from '@project-serum/associated-token';
|
||||||
|
import { parseTokenMintData } from '../../../utils/tokens';
|
||||||
|
import BN from 'bn.js';
|
||||||
|
import { refreshAllCaches } from '../../../utils/fetch-loop';
|
||||||
|
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
export function PoolAdminPanel({ poolInfo }: { poolInfo: PoolInfo }) {
|
||||||
|
return (
|
||||||
|
<FloatingElement>
|
||||||
|
<Tabs>
|
||||||
|
<TabPane tab="Pause/Unpause" key="pause">
|
||||||
|
<PauseUnpauseTab poolInfo={poolInfo} />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Add Token" key="addAsset">
|
||||||
|
<AddAssetTab poolInfo={poolInfo} />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Remove Token" key="removeAsset">
|
||||||
|
<RemoveAssetTab poolInfo={poolInfo} />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Deposit" key="deposit">
|
||||||
|
<DepositTab poolInfo={poolInfo} />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Withdraw" key="withdraw">
|
||||||
|
<WithdrawTab poolInfo={poolInfo} />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Change Fee" key="updateFee">
|
||||||
|
<UpdateFeeTab poolInfo={poolInfo} />
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</FloatingElement>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabParams {
|
||||||
|
poolInfo: PoolInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PauseUnpauseTab({ poolInfo }: TabParams) {
|
||||||
|
const connection = useConnection();
|
||||||
|
const { wallet, connected } = useWallet();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
async function sendPause() {
|
||||||
|
if (!connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const transaction = new Transaction();
|
||||||
|
transaction.add(AdminControlledPoolInstructions.pause(poolInfo));
|
||||||
|
await sendTransaction({ connection, wallet, transaction });
|
||||||
|
} catch (e) {
|
||||||
|
notify({
|
||||||
|
message: 'Error pausing pool: ' + e.message,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendUnpause() {
|
||||||
|
if (!connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const transaction = new Transaction();
|
||||||
|
transaction.add(AdminControlledPoolInstructions.unpause(poolInfo));
|
||||||
|
await sendTransaction({ connection, wallet, transaction });
|
||||||
|
} catch (e) {
|
||||||
|
notify({
|
||||||
|
message: 'Error unpausing pool: ' + e.message,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={sendPause} disabled={submitting}>
|
||||||
|
Pause
|
||||||
|
</Button>{' '}
|
||||||
|
<Button onClick={sendUnpause} disabled={submitting}>
|
||||||
|
Unpause
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddAssetTab({ poolInfo }: TabParams) {
|
||||||
|
const connection = useConnection();
|
||||||
|
const [address, setAddress] = useState('');
|
||||||
|
const { wallet, connected } = useWallet();
|
||||||
|
const canSubmit = connected && address;
|
||||||
|
const [onSubmit, submitting] = useOnSubmitHandler(
|
||||||
|
'adding asset to pool',
|
||||||
|
async () => {
|
||||||
|
const mintAddress = new PublicKey(address);
|
||||||
|
const vaultAddress = await getAssociatedTokenAddress(
|
||||||
|
poolInfo.state.vaultSigner,
|
||||||
|
mintAddress,
|
||||||
|
);
|
||||||
|
const transaction = new Transaction();
|
||||||
|
if (!(await connection.getAccountInfo(vaultAddress))) {
|
||||||
|
transaction.add(
|
||||||
|
await createAssociatedTokenAccount(
|
||||||
|
wallet.publicKey,
|
||||||
|
poolInfo.state.vaultSigner,
|
||||||
|
mintAddress,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
transaction.add(
|
||||||
|
AdminControlledPoolInstructions.addAsset(poolInfo, vaultAddress),
|
||||||
|
);
|
||||||
|
return transaction;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
addonBefore={<>Token Mint Address</>}
|
||||||
|
value={address}
|
||||||
|
onChange={(e) => setAddress(e.target.value.trim())}
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<SubmitButton canSubmit={canSubmit} submitting={submitting} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RemoveAssetTab({ poolInfo }: TabParams) {
|
||||||
|
const [address, setAddress] = useState('');
|
||||||
|
const { connected } = useWallet();
|
||||||
|
const canSubmit = connected && address;
|
||||||
|
const [onSubmit, submitting] = useOnSubmitHandler(
|
||||||
|
'removing asset from pool',
|
||||||
|
async () => {
|
||||||
|
const mintAddress = new PublicKey(address);
|
||||||
|
const vaultAddress = poolInfo.state.assets.find((asset) =>
|
||||||
|
asset.mint.equals(mintAddress),
|
||||||
|
)?.vaultAddress;
|
||||||
|
if (!vaultAddress) {
|
||||||
|
throw new Error('Asset not in pool');
|
||||||
|
}
|
||||||
|
const transaction = new Transaction();
|
||||||
|
transaction.add(
|
||||||
|
AdminControlledPoolInstructions.removeAsset(poolInfo, vaultAddress),
|
||||||
|
);
|
||||||
|
return transaction;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
addonBefore={<>Token Mint Address</>}
|
||||||
|
value={address}
|
||||||
|
onChange={(e) => setAddress(e.target.value.trim())}
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<SubmitButton canSubmit={canSubmit} submitting={submitting} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DepositTab({ poolInfo }: TabParams) {
|
||||||
|
const [address, setAddress] = useState('');
|
||||||
|
const [quantity, setQuantity] = useState('');
|
||||||
|
|
||||||
|
const connection = useConnection();
|
||||||
|
const { wallet, connected } = useWallet();
|
||||||
|
const [tokenAccounts] = useTokenAccounts();
|
||||||
|
const canSubmit =
|
||||||
|
connected && address && tokenAccounts && parseFloat(quantity);
|
||||||
|
|
||||||
|
const [onSubmit, submitting] = useOnSubmitHandler(
|
||||||
|
'depositing to pool',
|
||||||
|
async () => {
|
||||||
|
const mintAddress = new PublicKey(address);
|
||||||
|
const vaultAddress = poolInfo.state.assets.find((asset) =>
|
||||||
|
asset.mint.equals(mintAddress),
|
||||||
|
)?.vaultAddress;
|
||||||
|
if (!vaultAddress) {
|
||||||
|
throw new Error('Asset not in pool');
|
||||||
|
}
|
||||||
|
|
||||||
|
const walletTokenAccount = getSelectedTokenAccountForMint(
|
||||||
|
tokenAccounts,
|
||||||
|
mintAddress,
|
||||||
|
);
|
||||||
|
if (!walletTokenAccount) {
|
||||||
|
throw new Error('Asset not in wallet');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mintAccountInfo = await connection.getAccountInfo(mintAddress);
|
||||||
|
if (!mintAccountInfo) {
|
||||||
|
throw new Error('Mint not found');
|
||||||
|
}
|
||||||
|
const mintDecimals = parseTokenMintData(mintAccountInfo.data).decimals;
|
||||||
|
const parsedQuantity = Math.round(
|
||||||
|
parseFloat(quantity) * 10 ** mintDecimals,
|
||||||
|
);
|
||||||
|
|
||||||
|
const transaction = new Transaction();
|
||||||
|
transaction.add(
|
||||||
|
TokenInstructions.transfer({
|
||||||
|
source: walletTokenAccount.pubkey,
|
||||||
|
destination: vaultAddress,
|
||||||
|
amount: parsedQuantity,
|
||||||
|
owner: wallet.publicKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return transaction;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
addonBefore={<>Token Mint Address</>}
|
||||||
|
value={address}
|
||||||
|
onChange={(e) => setAddress(e.target.value.trim())}
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
addonBefore={<>Quantity</>}
|
||||||
|
value={quantity}
|
||||||
|
onChange={(e) => setQuantity(e.target.value.trim())}
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<SubmitButton canSubmit={canSubmit} submitting={submitting} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WithdrawTab({ poolInfo }: TabParams) {
|
||||||
|
const [address, setAddress] = useState('');
|
||||||
|
const [quantity, setQuantity] = useState('');
|
||||||
|
|
||||||
|
const connection = useConnection();
|
||||||
|
const { wallet, connected } = useWallet();
|
||||||
|
const [tokenAccounts] = useTokenAccounts();
|
||||||
|
const canSubmit =
|
||||||
|
connected && address && tokenAccounts && parseFloat(quantity);
|
||||||
|
|
||||||
|
const [onSubmit, submitting] = useOnSubmitHandler(
|
||||||
|
'withdrawing from pool',
|
||||||
|
async () => {
|
||||||
|
const mintAddress = new PublicKey(address);
|
||||||
|
const vaultAddress = poolInfo.state.assets.find((asset) =>
|
||||||
|
asset.mint.equals(mintAddress),
|
||||||
|
)?.vaultAddress;
|
||||||
|
if (!vaultAddress) {
|
||||||
|
throw new Error('Asset not in pool');
|
||||||
|
}
|
||||||
|
|
||||||
|
const walletTokenAccount = getSelectedTokenAccountForMint(
|
||||||
|
tokenAccounts,
|
||||||
|
mintAddress,
|
||||||
|
);
|
||||||
|
if (!walletTokenAccount) {
|
||||||
|
throw new Error('Asset not in wallet');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mintAccountInfo = await connection.getAccountInfo(mintAddress);
|
||||||
|
if (!mintAccountInfo) {
|
||||||
|
throw new Error('Mint not found');
|
||||||
|
}
|
||||||
|
const mintDecimals = parseTokenMintData(mintAccountInfo.data).decimals;
|
||||||
|
const parsedQuantity = Math.round(
|
||||||
|
parseFloat(quantity) * 10 ** mintDecimals,
|
||||||
|
);
|
||||||
|
|
||||||
|
const transaction = new Transaction();
|
||||||
|
transaction.add(
|
||||||
|
AdminControlledPoolInstructions.approveDelegate(
|
||||||
|
poolInfo,
|
||||||
|
vaultAddress,
|
||||||
|
wallet.publicKey,
|
||||||
|
new BN(parsedQuantity),
|
||||||
|
),
|
||||||
|
TokenInstructions.transfer({
|
||||||
|
source: vaultAddress,
|
||||||
|
destination: walletTokenAccount.pubkey,
|
||||||
|
amount: parsedQuantity,
|
||||||
|
owner: wallet.publicKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return transaction;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
addonBefore={<>Token Mint Address</>}
|
||||||
|
value={address}
|
||||||
|
onChange={(e) => setAddress(e.target.value.trim())}
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
addonBefore={<>Quantity</>}
|
||||||
|
value={quantity}
|
||||||
|
onChange={(e) => setQuantity(e.target.value.trim())}
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<SubmitButton canSubmit={canSubmit} submitting={submitting} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateFeeTab({ poolInfo }: TabParams) {
|
||||||
|
const [feeRate, setFeeRate] = useState('');
|
||||||
|
|
||||||
|
const { connected } = useWallet();
|
||||||
|
const [tokenAccounts] = useTokenAccounts();
|
||||||
|
const canSubmit = connected && tokenAccounts && parseFloat(feeRate);
|
||||||
|
|
||||||
|
const [onSubmit, submitting] = useOnSubmitHandler(
|
||||||
|
'changing pool fee',
|
||||||
|
async () => {
|
||||||
|
const transaction = new Transaction();
|
||||||
|
transaction.add(
|
||||||
|
AdminControlledPoolInstructions.updateFee(
|
||||||
|
poolInfo,
|
||||||
|
Math.round(parseFloat(feeRate) * 1_000_000),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return transaction;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Input
|
||||||
|
addonBefore={<>Fee Rate</>}
|
||||||
|
value={feeRate}
|
||||||
|
onChange={(e) => setFeeRate(e.target.value.trim())}
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<SubmitButton canSubmit={canSubmit} submitting={submitting} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useOnSubmitHandler(
|
||||||
|
description: string,
|
||||||
|
makeTransaction: () => Promise<Transaction | null | undefined>,
|
||||||
|
refresh = false,
|
||||||
|
): [(FormEvent) => void, boolean] {
|
||||||
|
const connection = useConnection();
|
||||||
|
const { wallet, connected } = useWallet();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
async function onSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
if (!connected) {
|
||||||
|
throw new Error('Wallet not connected');
|
||||||
|
}
|
||||||
|
const transaction = await makeTransaction();
|
||||||
|
if (!transaction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sendTransaction({ connection, wallet, transaction });
|
||||||
|
if (refresh) {
|
||||||
|
refreshAllCaches();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
notify({
|
||||||
|
message: `Error ${description}: ${e.message}`,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [onSubmit, submitting];
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubmitButton({ canSubmit, submitting }) {
|
||||||
|
const { connected } = useWallet();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
htmlType="submit"
|
||||||
|
type="primary"
|
||||||
|
disabled={!canSubmit || submitting}
|
||||||
|
>
|
||||||
|
{!connected ? 'Wallet not connected' : 'Submit'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -15,18 +15,18 @@ interface PoolInfoProps {
|
||||||
mintInfo: MintInfo;
|
mintInfo: MintInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const feeFormat = new Intl.NumberFormat(undefined, {
|
||||||
|
style: 'percent',
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 6,
|
||||||
|
});
|
||||||
|
|
||||||
export default function PoolInfoPanel({ poolInfo, mintInfo }: PoolInfoProps) {
|
export default function PoolInfoPanel({ poolInfo, mintInfo }: PoolInfoProps) {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
|
|
||||||
const [totalBasket] = useAsyncData(
|
const [totalBasket] = useAsyncData(
|
||||||
() => getPoolBasket(connection, poolInfo, { redeem: mintInfo.supply }),
|
() => getPoolBasket(connection, poolInfo, { redeem: mintInfo.supply }),
|
||||||
tuple(
|
tuple(getPoolBasket, connection, poolInfo, 'total', mintInfo),
|
||||||
getPoolBasket,
|
|
||||||
connection,
|
|
||||||
poolInfo.address.toBase58(),
|
|
||||||
'total',
|
|
||||||
mintInfo.supply.toString(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -39,6 +39,14 @@ export default function PoolInfoPanel({ poolInfo, mintInfo }: PoolInfoProps) {
|
||||||
Pool token mint address:{' '}
|
Pool token mint address:{' '}
|
||||||
<Text copyable>{poolInfo.state.poolTokenMint.toBase58()}</Text>
|
<Text copyable>{poolInfo.state.poolTokenMint.toBase58()}</Text>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
{poolInfo.state.adminKey ? (
|
||||||
|
<Paragraph>
|
||||||
|
Pool admin: <Text copyable>{poolInfo.state.adminKey.toBase58()}</Text>
|
||||||
|
</Paragraph>
|
||||||
|
) : null}
|
||||||
|
<Paragraph>
|
||||||
|
Fee rate: {feeFormat.format(poolInfo.state.feeRate / 1_000_000)}
|
||||||
|
</Paragraph>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
Total supply: {mintInfo.supply.toNumber() / 10 ** mintInfo.decimals}
|
Total supply: {mintInfo.supply.toNumber() / 10 ** mintInfo.decimals}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
|
@ -4,12 +4,18 @@ import { Col, PageHeader, Row, Spin, Typography } from 'antd';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import { useAccountInfo } from '../../../utils/connection';
|
import { useAccountInfo } from '../../../utils/connection';
|
||||||
import FloatingElement from '../../../components/layout/FloatingElement';
|
import FloatingElement from '../../../components/layout/FloatingElement';
|
||||||
import { decodePoolState, PoolInfo } from '@project-serum/pool';
|
import {
|
||||||
|
decodePoolState,
|
||||||
|
isAdminControlledPool,
|
||||||
|
PoolInfo,
|
||||||
|
} from '@project-serum/pool';
|
||||||
import PoolInfoPanel from './PoolInfoPanel';
|
import PoolInfoPanel from './PoolInfoPanel';
|
||||||
import { parseTokenMintData } from '../../../utils/tokens';
|
import { parseTokenMintData } from '../../../utils/tokens';
|
||||||
import PoolCreateRedeemPanel from './PoolCreateRedeemPanel';
|
import PoolCreateRedeemPanel from './PoolCreateRedeemPanel';
|
||||||
import PoolBalancesPanel from './PoolBalancesPanel';
|
import PoolBalancesPanel from './PoolBalancesPanel';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { PoolAdminPanel } from './PoolAdminPanel';
|
||||||
|
import { useWallet } from '../../../utils/wallet';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
@ -41,6 +47,7 @@ export default function PoolPage() {
|
||||||
() => (mintAccountInfo ? parseTokenMintData(mintAccountInfo.data) : null),
|
() => (mintAccountInfo ? parseTokenMintData(mintAccountInfo.data) : null),
|
||||||
[mintAccountInfo],
|
[mintAccountInfo],
|
||||||
);
|
);
|
||||||
|
const { wallet } = useWallet();
|
||||||
|
|
||||||
if (poolInfo && mintInfo) {
|
if (poolInfo && mintInfo) {
|
||||||
return (
|
return (
|
||||||
|
@ -60,6 +67,13 @@ export default function PoolPage() {
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
<PoolBalancesPanel poolInfo={poolInfo} />
|
<PoolBalancesPanel poolInfo={poolInfo} />
|
||||||
</Col>
|
</Col>
|
||||||
|
{wallet.connected &&
|
||||||
|
poolInfo.state.adminKey?.equals(wallet.publicKey) &&
|
||||||
|
isAdminControlledPool(poolInfo) ? (
|
||||||
|
<Col xs={24}>
|
||||||
|
<PoolAdminPanel poolInfo={poolInfo} />
|
||||||
|
</Col>
|
||||||
|
) : null}
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useLocalStorageState } from './utils';
|
import { useLocalStorageState } from './utils';
|
||||||
import { Account, AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
import { Account, AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
||||||
import React, { useContext, useEffect, useMemo } from 'react';
|
import React, { useContext, useEffect, useMemo, useRef } from 'react';
|
||||||
import { setCache, useAsyncData } from './fetch-loop';
|
import { setCache, useAsyncData } from './fetch-loop';
|
||||||
import tuple from 'immutable-tuple';
|
import tuple from 'immutable-tuple';
|
||||||
import { ConnectionContextValues, EndpointInfo } from './types';
|
import { ConnectionContextValues, EndpointInfo } from './types';
|
||||||
|
@ -137,14 +137,15 @@ export function useAccountInfo(
|
||||||
let currentItem = accountListenerCount.get(cacheKey);
|
let currentItem = accountListenerCount.get(cacheKey);
|
||||||
++currentItem.count;
|
++currentItem.count;
|
||||||
} else {
|
} else {
|
||||||
let previousData: Buffer | null = null;
|
let previousInfo: AccountInfo<Buffer> | null = null;
|
||||||
const subscriptionId = connection.onAccountChange(publicKey, (e) => {
|
const subscriptionId = connection.onAccountChange(publicKey, (info) => {
|
||||||
if (e.data) {
|
if (
|
||||||
if (!previousData || !previousData.equals(e.data)) {
|
!previousInfo ||
|
||||||
setCache(cacheKey, e);
|
!previousInfo.data.equals(info.data) ||
|
||||||
} else {
|
previousInfo.lamports !== info.lamports
|
||||||
}
|
) {
|
||||||
previousData = e.data;
|
previousInfo = info;
|
||||||
|
setCache(cacheKey, info);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
accountListenerCount.set(cacheKey, { count: 1, subscriptionId });
|
accountListenerCount.set(cacheKey, { count: 1, subscriptionId });
|
||||||
|
@ -161,7 +162,16 @@ export function useAccountInfo(
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [cacheKey]);
|
}, [cacheKey]);
|
||||||
return [accountInfo, loaded];
|
const previousInfoRef = useRef<AccountInfo<Buffer> | null>(null);
|
||||||
|
if (
|
||||||
|
accountInfo &&
|
||||||
|
(!previousInfoRef.current ||
|
||||||
|
!previousInfoRef.current.data.equals(accountInfo.data) ||
|
||||||
|
previousInfoRef.current.lamports !== accountInfo.lamports)
|
||||||
|
) {
|
||||||
|
previousInfoRef.current = accountInfo;
|
||||||
|
}
|
||||||
|
return [previousInfoRef.current, loaded];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAccountData(publicKey) {
|
export function useAccountData(publicKey) {
|
||||||
|
|
|
@ -177,6 +177,7 @@ class FetchLoops {
|
||||||
loop.removeListener(listener);
|
loop.removeListener(listener);
|
||||||
if (loop.stopped) {
|
if (loop.stopped) {
|
||||||
this.loops.delete(listener.cacheKey);
|
this.loops.delete(listener.cacheKey);
|
||||||
|
globalCache.delete(listener.cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -1530,16 +1530,31 @@
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
||||||
|
|
||||||
|
"@project-serum/associated-token@0.1.0", "@project-serum/associated-token@^0.1.0":
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@project-serum/associated-token/-/associated-token-0.1.0.tgz#ee817997c24e1089c5b99e370216ecc2d48e6d8b"
|
||||||
|
integrity sha512-sHDKQoT36TZl5aJct/pEbRtLUX6UXwnkGaA/ibHXkvDkQrtHjtkXEkkwmdhNZQsoXkSaUJdK62jh4c/BUMoZbg==
|
||||||
|
|
||||||
"@project-serum/awesome-serum@1.0.1":
|
"@project-serum/awesome-serum@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@project-serum/awesome-serum/-/awesome-serum-1.0.1.tgz#185a348cb57eb00592aee4f8c730b65cc9ae7a22"
|
resolved "https://registry.yarnpkg.com/@project-serum/awesome-serum/-/awesome-serum-1.0.1.tgz#185a348cb57eb00592aee4f8c730b65cc9ae7a22"
|
||||||
integrity sha512-WbykMEX2Ja1oeY3n7cVK79XMBxFgWsWxDMxfJ7GL+/88R568vwPvhP57HrBgULZmwu/Zt5CmMWjf6oeD1E621g==
|
integrity sha512-WbykMEX2Ja1oeY3n7cVK79XMBxFgWsWxDMxfJ7GL+/88R568vwPvhP57HrBgULZmwu/Zt5CmMWjf6oeD1E621g==
|
||||||
|
|
||||||
"@project-serum/pool@^0.1.1":
|
"@project-serum/borsh@^0.0.1-alpha.0":
|
||||||
version "0.1.3"
|
version "0.0.1-alpha.0"
|
||||||
resolved "https://registry.yarnpkg.com/@project-serum/pool/-/pool-0.1.3.tgz#9f99e6046f4565e38896934b24d447eae1527a62"
|
resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.0.1-alpha.0.tgz#61643cc686c31c6a4f483b3cc069dee5350b463a"
|
||||||
integrity sha512-wAO65EccmCCMmgolvVXIc3JbSapWxLWALuVuhbHiiYQjTCMolAYvGSzeJwur7Z7MjTuRJMc8z9/6rYNOQSAwaw==
|
integrity sha512-11eEM7LAwU+NW2Ha7Cc9nRNiTUuvT7zHlcDvAMEVxZbpZXQd09r6xEYgTpK87p9AcoTr56F36Ww5OvPO/LoJHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
bn.js "^5.1.2"
|
||||||
|
buffer-layout "^1.2.0"
|
||||||
|
|
||||||
|
"@project-serum/pool@^0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@project-serum/pool/-/pool-0.2.0.tgz#ce79b63a58e2dc7f5ae1387948ee1d91cca64b01"
|
||||||
|
integrity sha512-Gsd1G+S/7qx/2c49zIG1WF7xjElghRsVXUQewi10Rs7zY0ryS0jmIKHf6pPxn4Bpc0GKZmENrvwe2tChgY3+CA==
|
||||||
|
dependencies:
|
||||||
|
"@project-serum/associated-token" "^0.1.0"
|
||||||
|
"@project-serum/borsh" "^0.0.1-alpha.0"
|
||||||
"@project-serum/serum" "^0.13.8"
|
"@project-serum/serum" "^0.13.8"
|
||||||
bn.js "^5.1.2"
|
bn.js "^5.1.2"
|
||||||
buffer-layout "^1.2.0"
|
buffer-layout "^1.2.0"
|
||||||
|
|
Loading…
Reference in New Issue