Info popover

This commit is contained in:
armaniferrante 2021-05-14 11:30:21 -07:00
parent 94ded3ab69
commit dc5abad2c0
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
7 changed files with 340 additions and 106 deletions

160
src/components/Info.tsx Normal file
View File

@ -0,0 +1,160 @@
import { makeStyles, Typography, Link, Popover } from "@material-ui/core";
import { Info } from "@material-ui/icons";
import PopupState, { bindTrigger, bindPopover } from "material-ui-popup-state";
import { useFair } from "./context/Dex";
import { useTokenList } from "./context/TokenList";
import { useSwapContext } from "./context/Swap";
import { useMint } from "./context/Mint";
import { useMarketRoute } from "./context/Dex";
const useStyles = makeStyles((theme) => ({
infoLabel: {
marginTop: "10px",
marginBottom: "10px",
display: "flex",
justifyContent: "space-between",
marginLeft: "5px",
marginRight: "5px",
},
fairPriceLabel: {
marginRight: "10px",
display: "flex",
justifyContent: "center",
flexDirection: "column",
color: theme.palette.text.secondary,
},
}));
export function InfoLabel() {
const styles = useStyles();
const { fromMint, toMint } = useSwapContext();
const fromMintInfo = useMint(fromMint);
const fair = useFair(fromMint, toMint);
const tokenList = useTokenList();
let fromTokenInfo = tokenList.filter(
(t) => t.address === fromMint.toString()
)[0];
let toTokenInfo = tokenList.filter((t) => t.address === toMint.toString())[0];
return (
<div className={styles.infoLabel}>
<Typography color="textSecondary"></Typography>
<div style={{ display: "flex" }}>
<div className={styles.fairPriceLabel}>
{fair !== undefined
? `1 ${toTokenInfo.symbol} = ${fair.toFixed(
fromMintInfo?.decimals
)} ${fromTokenInfo.symbol}`
: `-`}
</div>
<InfoPopover />
</div>
</div>
);
}
function InfoPopover() {
const { fromMint, toMint } = useSwapContext();
const route = useMarketRoute(fromMint, toMint);
const tokenList = useTokenList();
const fromMintTicker = tokenList
.filter((t) => t.address === fromMint.toString())
.map((t) => t.symbol)[0];
const toMintTicker = tokenList
.filter((t) => t.address === toMint.toString())
.map((t) => t.symbol)[0];
const addresses = [
{ ticker: fromMintTicker, mint: fromMint },
{ ticker: toMintTicker, mint: toMint },
];
return (
<PopupState variant="popover">
{
//@ts-ignore
(popupState) => (
<div style={{ display: "flex" }}>
<Info {...bindTrigger(popupState)} />
<Popover
{...bindPopover(popupState)}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
PaperProps={{ style: { borderRadius: "10px" } }}
disableRestoreFocus
>
<div style={{ padding: "15px", width: "250px" }}>
<div>
<Typography
color="textSecondary"
style={{ fontWeight: "bold", marginBottom: "5px" }}
>
Trade Route
</Typography>
{route.map((market) => {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: "5px",
}}
>
<Link
href={`https://dex.projectserum.com/#/market/${market.address.toString()}`}
target="_blank"
rel="noopener"
>
{market.name}
</Link>
<code style={{ marginLeft: "10px" }}>
{market.fair}
</code>
</div>
);
})}
</div>
<div style={{ marginTop: "15px" }}>
<Typography
color="textSecondary"
style={{ fontWeight: "bold", marginBottom: "5px" }}
>
Tokens
</Typography>
{addresses.map((address) => {
return (
<div
style={{
marginTop: "5px",
display: "flex",
justifyContent: "space-between",
}}
>
<Link
href={`https://explorer.solana.com/address/${address.mint.toString()}`}
target="_blank"
rel="noopener"
>
{address.ticker}
</Link>
<code style={{ width: "128px", overflow: "hidden" }}>
{address.mint.toString()}
</code>
</div>
);
})}
</div>
</div>
</Popover>
</div>
)
}
</PopupState>
);
}

View File

@ -22,6 +22,7 @@ import {
MenuItem,
TextField,
InputAdornment,
Link,
} from "@material-ui/core";
import { SettingsOutlined as Settings } from "@material-ui/icons";
import PopupState, { bindTrigger, bindPopover } from "material-ui-popup-state";
@ -154,12 +155,12 @@ function OpenOrdersAccounts() {
<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 Account</TableCell>
<TableCell align="right">Action</TableCell>
<TableCell align="center">Open Orders Account</TableCell>
<TableCell align="center">Base Used</TableCell>
<TableCell align="center">Base Free</TableCell>
<TableCell align="center">Quote Used</TableCell>
<TableCell align="center">Quote Free</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
@ -212,24 +213,17 @@ function OpenOrdersRow({
return (
<TableRow key={market.toString()}>
<TableCell component="th" scope="row">
{marketName}
<Typography>
<Link
href={`https://dex.projectserum.com/#/market/${market.toString()}`}
target="_blank"
rel="noopener"
>
{marketName}
</Link>
</Typography>
</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">
<TableCell align="center">
<Select
value={ooAccount.address.toString()}
onChange={(e) =>
@ -249,7 +243,22 @@ function OpenOrdersRow({
})}
</Select>
</TableCell>
<TableCell align="right">
<TableCell align="center">
{toDisplay(base, ooAccount.baseTokenTotal.sub(ooAccount.baseTokenFree))}
</TableCell>
<TableCell align="center">
{toDisplay(base, ooAccount.baseTokenFree)}
</TableCell>
<TableCell align="center">
{toDisplay(
quote,
ooAccount.quoteTokenTotal.sub(ooAccount.quoteTokenFree)
)}
</TableCell>
<TableCell align="center">
{toDisplay(quote, ooAccount.quoteTokenFree)}
</TableCell>
<TableCell align="center">
<Button
color="secondary"
disabled={closeDisabled}

View File

@ -11,7 +11,7 @@ import {
Typography,
TextField,
} from "@material-ui/core";
import { Info, ExpandMore } from "@material-ui/icons";
import { ExpandMore } from "@material-ui/icons";
import { SwapContextProvider, useSwapContext } from "./context/Swap";
import { DexContextProvider } from "./context/Dex";
import { MintContextProvider, useMint } from "./context/Mint";
@ -19,6 +19,7 @@ import { TokenListContextProvider, useTokenList } from "./context/TokenList";
import { TokenContextProvider, useOwnedTokenAccount } from "./context/Token";
import TokenDialog from "./TokenDialog";
import { SettingsButton } from "./Settings";
import { InfoLabel } from "./Info";
const useStyles = makeStyles(() => ({
card: {
@ -46,14 +47,6 @@ const useStyles = makeStyles(() => ({
marginLeft: "auto",
marginRight: "auto",
},
auxilliaryLabel: {
marginTop: "10px",
marginBottom: "10px",
display: "flex",
justifyContent: "space-between",
marginLeft: "5px",
marginRight: "5px",
},
}));
export default function Swap({
@ -89,9 +82,9 @@ function SwapCard({ style }: { style?: any }) {
<SwapHeader />
<div className={styles.cardContent}>
<SwapFromForm />
<SwapToFromButton />
<ArrowButton />
<SwapToForm />
<AuxilliaryLabel />
<InfoLabel />
<SwapButton />
</div>
</Card>
@ -123,41 +116,7 @@ function SwapHeader() {
);
}
function AuxilliaryLabel() {
const styles = useStyles();
const { fromMint, toMint, fromAmount, toAmount } = useSwapContext();
const toPrice = (fromAmount / toAmount).toFixed(6); // TODO: decimals per mint type.
const tokenList = useTokenList();
let fromTokenInfo = tokenList.filter(
(t) => t.address === fromMint.toString()
)[0];
let toTokenInfo = tokenList.filter((t) => t.address === toMint.toString())[0];
return (
<div className={styles.auxilliaryLabel}>
<Typography color="textSecondary"></Typography>
<div style={{ display: "flex" }}>
<div
style={{
marginRight: "10px",
display: "flex",
justifyContent: "center",
flexDirection: "column",
}}
>
{fromAmount !== 0 && toAmount !== 0
? `1 ${toTokenInfo.symbol} = ${toPrice} ${fromTokenInfo.symbol}`
: `-`}
</div>
<Info />
</div>
</div>
);
}
export function SwapToFromButton() {
export function ArrowButton() {
const styles = useStyles();
const { swapToFromMints } = useSwapContext();
return (

View File

@ -11,8 +11,9 @@ import {
Typography,
} from "@material-ui/core";
import { TokenIcon } from "./Swap";
import { useSwapContext, USDC_MINT, USDT_MINT } from "./context/Swap";
import { useSwapContext } from "./context/Swap";
import { useTokenList } from "./context/TokenList";
import { USDC_MINT, USDT_MINT } from "../utils/pubkeys";
const useStyles = makeStyles((theme) => ({
dialogContent: {

View File

@ -2,19 +2,24 @@ 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 { Market, OpenOrders } from "@project-serum/serum";
import {
Market,
OpenOrders,
Orderbook as OrderbookSide,
} from "@project-serum/serum";
import { PublicKey } from "@solana/web3.js";
import { DEX_PID } from "../../utils/pubkeys";
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
const _DexContext = React.createContext<DexContext | null>(null);
type DexContext = {
// Maps market address to open orders accounts.
openOrders: Map<string, Array<OpenOrders>>;
marketCache: Map<string, Market>;
setMarketCache: (c: Map<string, Market>) => void;
orderbookCache: Map<string, Orderbook>;
setOrderbookCache: (c: Map<string, Orderbook>) => void;
provider: Provider;
};
const _DexContext = React.createContext<DexContext | null>(null);
export function DexContextProvider(props: any) {
const [ooAccounts, setOoAccounts] = useState<Map<string, Array<OpenOrders>>>(
@ -23,11 +28,16 @@ export function DexContextProvider(props: any) {
const [marketCache, setMarketCache] = useState<Map<string, Market>>(
new Map()
);
const [orderbookCache, setOrderbookCache] = useState<Map<string, Orderbook>>(
new Map()
);
const provider = props.provider;
// Two operations:
//
// 1. Fetch all open orders accounts for the connected wallet.
// 2. Batch fetch all market accounts.
// 2. Batch fetch all market accounts for those open orders.
//
useEffect(() => {
OpenOrders.findForOwner(
provider.connection,
@ -84,6 +94,8 @@ export function DexContextProvider(props: any) {
openOrders: ooAccounts,
marketCache,
setMarketCache,
orderbookCache,
setOrderbookCache,
provider,
}}
>
@ -92,19 +104,22 @@ export function DexContextProvider(props: any) {
);
}
export function useOpenOrders(): Map<string, Array<OpenOrders>> {
function useDexContext(): DexContext {
const ctx = useContext(_DexContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx;
}
export function useOpenOrders(): Map<string, Array<OpenOrders>> {
const ctx = useDexContext();
return ctx.openOrders;
}
// Lazy load a given market.
export function useMarket(market: PublicKey): Market | undefined {
const ctx = useContext(_DexContext);
if (ctx === null) {
throw new Error("Context not available");
}
const ctx = useDexContext();
const asyncMarket = useAsync(async () => {
if (ctx.marketCache.get(market.toString())) {
@ -130,3 +145,74 @@ export function useMarket(market: PublicKey): Market | undefined {
return undefined;
}
// Lazy load the orderbook for a given market.
export function useOrderbook(market: PublicKey): Orderbook | undefined {
const ctx = useDexContext();
const marketClient = useMarket(market);
const asyncOrderbook = useAsync(async () => {
if (!marketClient) {
return undefined;
}
if (ctx.orderbookCache.get(market.toString())) {
return ctx.orderbookCache.get(market.toString());
}
const [bids, asks] = await Promise.all([
marketClient.loadBids(ctx.provider.connection),
marketClient.loadAsks(ctx.provider.connection),
]);
const orderbook = {
bids,
asks,
};
const cache = new Map(ctx.orderbookCache);
cache.set(market.toString(), orderbook);
ctx.setOrderbookCache(cache);
return orderbook;
}, [ctx.provider.connection, market, marketClient]);
if (asyncOrderbook.result) {
return asyncOrderbook.result;
}
return undefined;
}
export function useMarketRoute(
fromMint: PublicKey,
toMint: PublicKey
): Array<{ address: PublicKey; name: string; fair: number }> {
// todo
return [
{
address: new PublicKey("ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA"),
name: "SRM / USDC",
fair: 0.5,
},
{
address: new PublicKey("J7cPYBrXVy8Qeki2crZkZavcojf2sMRyQU7nx438Mf8t"),
name: "MATH / USDC",
fair: 1.23,
},
];
}
// Fair price for a theoretical toMint/fromMint market. I.e., the number
// of `fromMint` tokens to purchase a single `toMint` token.
export function useFair(
fromMint: PublicKey,
toMint: PublicKey
): number | undefined {
// todo
return 0.5;
}
type Orderbook = {
bids: OrderbookSide;
asks: OrderbookSide;
};

View File

@ -2,15 +2,25 @@ import React, { useContext, useState } from "react";
import { Swap as SwapClient } from "@project-serum/swap";
import { PublicKey } from "@solana/web3.js";
import { MintInfo } from "@solana/spl-token";
import { SRM_MINT, USDC_MINT } from "../../utils/pubkeys";
import { useFair } from "./Dex";
const SRM_MINT = new PublicKey("SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt");
export const USDC_MINT = new PublicKey(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
);
export const USDT_MINT = new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
);
export type SwapContext = {
swapClient: SwapClient;
fromMint: PublicKey;
setFromMint: (m: PublicKey) => void;
toMint: PublicKey;
setToMint: (m: PublicKey) => void;
fromAmount: number;
setFromAmount: (a: number) => void;
toAmount: number;
setToAmount: (a: number) => void;
swapToFromMints: () => void;
fromMintAccount?: MintInfo;
toMintAccount?: MintInfo;
slippage: number;
setSlippage: (n: number) => void;
};
const _SwapContext = React.createContext<null | SwapContext>(null);
export function SwapContextProvider(props: any) {
@ -21,6 +31,7 @@ export function SwapContextProvider(props: any) {
const [toAmount, _setToAmount] = useState(0);
// Percent units.
const [slippage, setSlippage] = useState(0.5);
const fair = useFair(fromMint, toMint);
const swapToFromMints = () => {
const oldFrom = fromMint;
@ -34,11 +45,19 @@ export function SwapContextProvider(props: any) {
};
const setFromAmount = (amount: number) => {
if (fair === undefined) {
throw new Error("Fair price not found");
}
_setFromAmount(amount);
_setToAmount(amount / fair);
};
const setToAmount = (amount: number) => {
if (fair === undefined) {
throw new Error("Fair price not found");
}
_setToAmount(amount);
_setFromAmount(amount * fair);
};
return (
@ -70,20 +89,3 @@ export function useSwapContext(): SwapContext {
}
return ctx;
}
export type SwapContext = {
swapClient: SwapClient;
fromMint: PublicKey;
setFromMint: (m: PublicKey) => void;
toMint: PublicKey;
setToMint: (m: PublicKey) => void;
fromAmount: number;
setFromAmount: (a: number) => void;
toAmount: number;
setToAmount: (a: number) => void;
swapToFromMints: () => void;
fromMintAccount?: MintInfo;
toMintAccount?: MintInfo;
slippage: number;
setSlippage: (n: number) => void;
};

17
src/utils/pubkeys.ts Normal file
View File

@ -0,0 +1,17 @@
import { PublicKey } from "@solana/web3.js";
export const SRM_MINT = new PublicKey(
"SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt"
);
export const USDC_MINT = new PublicKey(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
);
export const USDT_MINT = new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
);
export const DEX_PID = new PublicKey(
"9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
);