Open orders dialog

This commit is contained in:
armaniferrante 2021-05-13 01:11:13 -07:00
parent 527177117f
commit 4a239d8a08
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
7 changed files with 444 additions and 99 deletions

View File

@ -6,6 +6,7 @@
"dependencies": {
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@project-serum/serum": "^0.13.34",
"@project-serum/sol-wallet-adapter": "^0.2.0",
"@project-serum/swap": "^0.1.0-alpha.2",
"@solana/spl-token": "^0.1.4",
@ -19,7 +20,9 @@
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"bs58": "^4.0.1",
"material-ui-popup-state": "^1.8.3",
"react": "^17.0.2",
"react-async-hook": "^3.6.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"typescript": "^4.1.2",

View File

@ -18,7 +18,7 @@ function App() {
preflightCommitment: "recent",
commitment: "recent",
};
const network = "https://api.mainnet-beta.solana.com";
const network = "https://solana-api.projectserum.com";
const wallet = new Wallet("https://www.sollet.io", network);
const connection = new Connection(network, opts.preflightCommitment);
const provider = new Provider(connection, wallet, opts);

View File

@ -1,5 +1,9 @@
import React, { useContext, useState, useEffect } from "react";
import { useAsync } from "react-async-hook";
import * as anchor from "@project-serum/anchor";
import { Provider } from "@project-serum/anchor";
import { Swap as SwapClient } from "@project-serum/swap";
import { Market, OpenOrders } from "@project-serum/serum";
import { PublicKey, Account } from "@solana/web3.js";
import {
AccountInfo as TokenAccount,
@ -17,6 +21,7 @@ export const USDC_MINT = new PublicKey(
export const USDT_MINT = new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
);
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
const SwapContext = React.createContext<null | SwapContext>(null);
@ -30,7 +35,6 @@ export function SwapContextProvider(props: any) {
const [toBalance, setToBalance] = useState(undefined);
const [minExpectedAmount, setMinExpectedAmount] = useState(0);
const [ownedTokenAccounts, setOwnedTokenAccounts] = useState(undefined);
const [mintCache, setMintCache] = useState(new Map<string, MintInfo>());
// Fetch all the owned token accounts for the wallet.
useEffect(() => {
@ -43,51 +47,6 @@ export function SwapContextProvider(props: any) {
swapClient.program.provider.connection,
]);
// Fetch the mint account infos not already in the cache.
useEffect(() => {
const fromMintClient = new Token(
swapClient.program.provider.connection,
fromMint,
TOKEN_PROGRAM_ID,
new Account()
);
const toMintClient = new Token(
swapClient.program.provider.connection,
toMint,
TOKEN_PROGRAM_ID,
new Account()
);
let promises = [];
if (mintCache.get(fromMint.toString())) {
promises.push(
(async (): Promise<MintInfo> => {
return mintCache.get(fromMint.toString()) as MintInfo;
})()
);
} else {
promises.push(fromMintClient.getMintInfo());
}
if (mintCache.get(toMint.toString())) {
promises.push(
(async (): Promise<MintInfo> => {
return mintCache.get(toMint.toString()) as MintInfo;
})()
);
} else {
promises.push(toMintClient.getMintInfo());
}
Promise.all(promises as [Promise<MintInfo>, Promise<MintInfo>]).then(
([fromMintInfo, toMintInfo]: [MintInfo, MintInfo]) => {
let cache = new Map(mintCache);
cache.set(fromMint.toString(), fromMintInfo);
cache.set(toMint.toString(), toMintInfo);
setMintCache(cache);
}
);
}, [fromMint, toMint]);
const swapToFromMints = () => {
const oldFrom = fromMint;
const oldFromAmount = fromAmount;
@ -116,7 +75,6 @@ export function SwapContextProvider(props: any) {
fromBalance,
toBalance,
ownedTokenAccounts,
mintCache,
}}
>
{props.children}
@ -151,7 +109,6 @@ export type SwapContext = {
ownedTokenAccounts:
| { publicKey: PublicKey; account: TokenAccount }[]
| undefined;
mintCache: Map<string, MintInfo>;
};
const TokenListContext = React.createContext<null | TokenListContext>(null);
@ -207,10 +164,154 @@ export function useOwnedTokenAccount(
return tokenAccounts[0];
}
export function useMintAccount(mint: PublicKey): MintInfo | undefined {
const ctx = useContext(SwapContext);
const MintContext = React.createContext<null | MintContext>(null);
type MintContext = {
mintCache: Map<string, MintInfo>;
setMintCache: (m: Map<string, MintInfo>) => void;
provider: Provider;
};
export function MintContextProvider(props: any) {
const provider = props.provider;
const [mintCache, setMintCache] = useState(new Map<string, MintInfo>());
return (
<MintContext.Provider
value={{
mintCache,
setMintCache,
provider,
}}
>
{props.children}
</MintContext.Provider>
);
}
export function useMint(mint: PublicKey): MintInfo | undefined | null {
const ctx = useContext(MintContext);
if (ctx === null) {
throw new Error("Mint context not found");
}
// Lazy load the mint account if needeed.
const asyncMintInfo = useAsync(async () => {
if (ctx.mintCache.get(mint.toString())) {
return ctx.mintCache.get(mint.toString());
}
const mintClient = new Token(
ctx.provider.connection,
mint,
TOKEN_PROGRAM_ID,
new Account()
);
const mintInfo = await mintClient.getMintInfo();
let cache = new Map(ctx.mintCache);
cache.set(mint.toString(), mintInfo);
ctx.setMintCache(cache);
return mintInfo;
}, [ctx.provider.connection, mint]);
if (asyncMintInfo.result) {
return asyncMintInfo.result;
}
return undefined;
}
const SerumDexContext = React.createContext<SerumDexContext | null>(null);
type SerumDexContext = {
// Maps market address to open orders accounts.
openOrders: Map<string, Array<OpenOrders>>;
marketCache: Map<string, Market>;
};
export function useOpenOrders(): Map<string, Array<OpenOrders>> {
const ctx = useContext(SerumDexContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx.mintCache.get(mint.toString());
return ctx.openOrders;
}
export function useMarket(market: PublicKey): Market | undefined {
const ctx = useContext(SerumDexContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx.marketCache.get(market.toString());
}
export function SerumDexContextProvider(props: any) {
const [ooAccounts, setOoAccounts] = useState<Map<string, Array<OpenOrders>>>(
new Map()
);
const [marketCache, setMarketCache] = useState<Map<string, Market>>(
new Map()
);
const provider = props.provider;
// Two operations:
// 1. Fetch all open orders accounts for the connected wallet.
// 2. Batch fetch all market accounts.
useEffect(() => {
OpenOrders.findForOwner(
provider.connection,
provider.wallet.publicKey,
DEX_PID
).then(async (openOrders) => {
const newOoAccounts = new Map();
let markets = new Set<string>();
openOrders.forEach((oo) => {
markets.add(oo.market.toString());
if (newOoAccounts.get(oo.market.toString())) {
newOoAccounts.get(oo.market.toString()).push(oo);
} else {
newOoAccounts.set(oo.market.toString(), [oo]);
}
});
if (markets.size > 100) {
// Punt request chunking until there's user demand.
throw new Error(
"Too many markets. Please file an issue to update this"
);
}
const marketAccounts = (
await anchor.utils.getMultipleAccounts(
provider.connection,
// @ts-ignore
[...markets].map((m) => new PublicKey(m))
)
).map((programAccount) => {
return {
publicKey: programAccount?.publicKey,
account: new Market(
Market.getLayout(DEX_PID).decode(programAccount?.account.data),
-1, // Not used so don't bother fetching.
-1, // Not used so don't bother fetching.
provider.opts,
DEX_PID
),
};
});
const newMarketCache = new Map(marketCache);
marketAccounts.forEach((m) => {
newMarketCache.set(m.publicKey!.toString(), m.account);
});
setMarketCache(newMarketCache);
setOoAccounts(newOoAccounts);
});
}, [provider.connection, provider.wallet.publicKey, DEX_PID]);
return (
<SerumDexContext.Provider
value={{
openOrders: ooAccounts,
marketCache,
}}
>
{props.children}
</SerumDexContext.Provider>
);
}

View File

@ -0,0 +1,242 @@
import { useState } from "react";
import { PublicKey } from "@solana/web3.js";
import { MintInfo } from "@solana/spl-token";
import { BN } from "@project-serum/anchor";
import { OpenOrders } from "@project-serum/serum";
import {
makeStyles,
Dialog,
DialogContent,
Paper,
Table,
TableRow,
TableHead,
TableCell,
TableBody,
TableContainer,
Popover,
IconButton,
Typography,
Button,
Select,
MenuItem,
} from "@material-ui/core";
import { SettingsOutlined as Settings } from "@material-ui/icons";
import PopupState, { bindTrigger, bindPopover } from "material-ui-popup-state";
import { useOpenOrders, useMarket, useMint, useTokenList } from "./Context";
const useStyles = makeStyles(() => ({
tab: {
width: "50%",
},
table: {},
settingsButton: {
padding: 0,
},
}));
export default function SettingsButton() {
const styles = useStyles();
const [showSettingsDialog, setShowSettingsDialog] = useState(false);
return (
<PopupState variant="popover">
{
//@ts-ignore
(popupState) => (
<div>
<IconButton
{...bindTrigger(popupState)}
className={styles.settingsButton}
>
<Settings />
</IconButton>
<Popover
{...bindPopover(popupState)}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
PaperProps={{ style: { borderRadius: "10px" } }}
>
<div style={{ padding: "15px", width: "305px", height: "285px" }}>
<Typography
color="textSecondary"
style={{ fontWeight: "bold" }}
>
Swap Settings
</Typography>
<div>
<div>Slippage</div>
<Button onClick={() => setShowSettingsDialog(true)}>
Manage Dex Accounts
</Button>
</div>
<SettingsDialog
open={showSettingsDialog}
onClose={() => setShowSettingsDialog(false)}
/>
</div>
</Popover>
</div>
)
}
</PopupState>
);
}
export function SettingsDialog({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
return (
<Dialog
maxWidth="lg"
open={open}
onClose={onClose}
PaperProps={{
style: {
borderRadius: "10px",
},
}}
>
<div>
<DialogContent style={{ paddingTop: 0 }}>
<OpenOrdersAccounts />
</DialogContent>
</div>
</Dialog>
);
}
function OpenOrdersAccounts() {
const styles = useStyles();
const openOrders = useOpenOrders();
return (
<TableContainer component={Paper} elevation={0}>
<Table className={styles.table} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Market</TableCell>
<TableCell align="right">Base Used</TableCell>
<TableCell align="right">Base Free</TableCell>
<TableCell align="right">Quote Used</TableCell>
<TableCell align="right">Quote Free</TableCell>
<TableCell align="right">Open Orders</TableCell>
<TableCell align="right">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Array.from(openOrders.entries()).map(([market, oos]) => {
return (
<OpenOrdersRow
key={market}
market={new PublicKey(market)}
openOrders={oos}
/>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}
function OpenOrdersRow({
market,
openOrders,
}: {
market: PublicKey;
openOrders: Array<OpenOrders>;
}) {
const [ooAccount, setOoAccount] = useState(openOrders[0]);
const marketClient = useMarket(market);
const tokenList = useTokenList();
const base = useMint(marketClient!.baseMintAddress);
const quote = useMint(marketClient!.quoteMintAddress);
const baseTicker = tokenList
.filter((t) => t.address === marketClient!.baseMintAddress.toString())
.map((t) => t.symbol)[0];
const quoteTicker = tokenList
.filter((t) => t.address === marketClient!.quoteMintAddress.toString())
.map((t) => t.symbol)[0];
const marketName =
baseTicker && quoteTicker
? `${baseTicker} / ${quoteTicker}`
: market.toString();
const closeDisabled =
ooAccount.baseTokenTotal.toNumber() +
ooAccount.quoteTokenTotal.toNumber() !==
0;
const closeOpenOrders = async () => {
// todo
};
return (
<TableRow key={market.toString()}>
<TableCell component="th" scope="row">
{marketName}
</TableCell>
<TableCell align="right">
{toDisplay(base, ooAccount.baseTokenTotal.sub(ooAccount.baseTokenFree))}
</TableCell>
<TableCell align="right">
{toDisplay(base, ooAccount.baseTokenFree)}
</TableCell>
<TableCell align="right">
{toDisplay(
quote,
ooAccount.quoteTokenTotal.sub(ooAccount.quoteTokenFree)
)}
</TableCell>
<TableCell align="right">
{toDisplay(quote, ooAccount.quoteTokenFree)}
</TableCell>
<TableCell align="right">
<Select
value={ooAccount.address.toString()}
onChange={(e) =>
setOoAccount(
openOrders.filter(
(oo) => oo.address.toString() === e.target.value
)[0]
)
}
>
{openOrders.map((oo) => {
return (
<MenuItem value={oo.address.toString()}>
{oo.address.toString()}
</MenuItem>
);
})}
</Select>
</TableCell>
<TableCell align="right">
<Button
color="secondary"
disabled={closeDisabled}
onClick={closeOpenOrders}
>
Close
</Button>
</TableCell>
</TableRow>
);
}
function toDisplay(mintInfo: MintInfo | undefined | null, value: BN): string {
if (!mintInfo) {
return value.toNumber().toString();
}
return (value.toNumber() / 10 ** mintInfo.decimals).toFixed(
mintInfo.decimals
);
}

View File

@ -7,23 +7,23 @@ import {
makeStyles,
Card,
Button,
Tabs,
Tab,
IconButton,
Paper,
Typography,
TextField,
} from "@material-ui/core";
import { Settings, Info, ExpandMore } from "@material-ui/icons";
import { Info, ExpandMore } from "@material-ui/icons";
import {
MintContextProvider,
SwapContextProvider,
TokenListContextProvider,
SerumDexContextProvider,
useSwapContext,
useTokenList,
useOwnedTokenAccount,
useMintAccount,
useMint,
} from "./Context";
import TokenDialog from "./TokenDialog";
import SettingsButton from "./SettingsDialog";
const useStyles = makeStyles(() => ({
card: {
@ -39,10 +39,6 @@ const useStyles = makeStyles(() => ({
tab: {
width: "50%",
},
settings: {
display: "flex",
flexDirection: "row-reverse",
},
settingsButton: {
padding: 0,
},
@ -76,11 +72,15 @@ export default function Swap({
}) {
const swapClient = new SwapClient(provider, tokenList);
return (
<SwapContextProvider swapClient={swapClient}>
<TokenListContextProvider tokenList={tokenList}>
<SwapInner style={style} />
</TokenListContextProvider>
</SwapContextProvider>
<MintContextProvider provider={provider}>
<SwapContextProvider swapClient={swapClient}>
<TokenListContextProvider tokenList={tokenList}>
<SerumDexContextProvider provider={provider}>
<SwapInner style={style} />
</SerumDexContextProvider>
</TokenListContextProvider>
</SwapContextProvider>
</MintContextProvider>
);
}
@ -140,7 +140,7 @@ function AuxilliaryLabel() {
return (
<div className={styles.auxilliaryLabel}>
<Typography color="textSecondary">Serum</Typography>
<Typography color="textSecondary"></Typography>
<div style={{ display: "flex" }}>
<div
style={{
@ -160,16 +160,6 @@ function AuxilliaryLabel() {
);
}
function TabSelector() {
const styles = useStyles();
return (
<Tabs>
<Tab label="Trade" className={styles.tab} />
<Tab label="Accounts" className={styles.tab} />
</Tabs>
);
}
export function SwapToFromButton() {
const styles = useStyles();
const { swapToFromMints } = useSwapContext();
@ -180,22 +170,10 @@ export function SwapToFromButton() {
);
}
function SettingsButton() {
const styles = useStyles();
return (
<div className={styles.settings}>
<IconButton className={styles.settingsButton}>
<Settings />
</IconButton>
</div>
);
}
function SwapFromForm() {
const { fromMint, setFromMint, fromAmount, setFromAmount } = useSwapContext();
return (
<SwapTokenForm
isEstimate={false}
mint={fromMint}
setMint={setFromMint}
amount={fromAmount}
@ -208,7 +186,6 @@ function SwapToForm() {
const { toMint, setToMint, toAmount, setToAmount } = useSwapContext();
return (
<SwapTokenForm
isEstimate={true}
mint={toMint}
setMint={setToMint}
amount={toAmount}
@ -218,13 +195,11 @@ function SwapToForm() {
}
function SwapTokenForm({
isEstimate,
mint,
setMint,
amount,
setAmount,
}: {
isEstimate: boolean;
mint: PublicKey;
setMint: (m: PublicKey) => void;
amount: number;
@ -232,10 +207,10 @@ function SwapTokenForm({
}) {
const [showTokenDialog, setShowTokenDialog] = useState(false);
const tokenAccount = useOwnedTokenAccount(mint);
const mintAccount = useMintAccount(mint);
const mintAccount = useMint(mint);
return (
<Paper elevation={0} variant="outlined">
<Paper elevation={0} variant="outlined" style={{ borderRadius: "10px" }}>
<div
style={{
height: "50px",
@ -283,7 +258,7 @@ function TokenButton({
}) {
return (
<Button onClick={onClick} style={{ width: "116px" }}>
<TokenIcon mint={mint} style={{ width: "25px" }} />
<TokenIcon mint={mint} style={{ width: "25px", borderRadius: "13px" }} />
<TokenName mint={mint} />
<ExpandMore />
</Button>
@ -335,7 +310,3 @@ function SwapButton() {
</Button>
);
}
function TokenSelector() {
return <div></div>;
}

View File

@ -67,6 +67,7 @@ export default function TokenDialog({
.concat([USDC_MINT, USDT_MINT])
.map((mint) => (
<TokenListItem
key={mint.toString()}
mint={mint}
onClick={(mint) => {
setMint(mint);

View File

@ -1472,6 +1472,13 @@
resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2"
integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==
"@material-ui/types@^4.1.1":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-4.1.1.tgz#b65e002d926089970a3271213a3ad7a21b17f02b"
integrity sha512-AN+GZNXytX9yxGi0JOfxHrRTbhFybjUJ05rnsBVjcB+16e466Z0Xe5IxawuOayVZgTBNDxmPKo5j4V6OnMtaSQ==
dependencies:
"@types/react" "*"
"@material-ui/utils@^4.11.2":
version "4.11.2"
resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.2.tgz#f1aefa7e7dff2ebcb97d31de51aecab1bb57540a"
@ -3615,6 +3622,11 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.6:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-css@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@ -7815,6 +7827,16 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
material-ui-popup-state@^1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/material-ui-popup-state/-/material-ui-popup-state-1.8.3.tgz#1e263682659c6b39af38c68c21ce4f316f2bb56e"
integrity sha512-qvrX5snjXwn3rv5i3+w/T1WF3NHRPZ6KzB/gOzh9cxowEvlWV0HkCSbPmEDTnfQt6s6r964cMV987QZ50eNhxA==
dependencies:
"@babel/runtime" "^7.12.5"
"@material-ui/types" "^4.1.1"
classnames "^2.2.6"
prop-types "^15.7.2"
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -9813,6 +9835,11 @@ react-app-polyfill@^2.0.0:
regenerator-runtime "^0.13.7"
whatwg-fetch "^3.4.1"
react-async-hook@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/react-async-hook/-/react-async-hook-3.6.2.tgz#360018e5cd6ecc8841152a79875841b9e49dbdba"
integrity sha512-RkwHCJ8V7I6umKZLHneapuTRWf+eO4LOj0qUwUDsSn27jrAOcW6ClbV3x22Z4hVxH9bA0zb7y+ozDJDJ8PnZoA==
react-dev-utils@^11.0.3:
version "11.0.4"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a"