Trade route fairs

This commit is contained in:
armaniferrante 2021-05-14 14:29:49 -07:00
parent dc5abad2c0
commit 3ad62023c8
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
8 changed files with 248 additions and 189 deletions

View File

@ -6,9 +6,10 @@
"dependencies": {
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@project-serum/anchor": "^0.5.1-beta.2",
"@project-serum/serum": "^0.13.34",
"@project-serum/sol-wallet-adapter": "^0.2.0",
"@project-serum/swap": "^0.1.0-alpha.2",
"@project-serum/swap": "^0.1.0-alpha.3",
"@solana/spl-token": "^0.1.4",
"@solana/spl-token-registry": "^0.2.86",
"@solana/web3.js": "^1.10.1",

View File

@ -1,11 +1,22 @@
import { makeStyles, Typography, Link, Popover } from "@material-ui/core";
import {
makeStyles,
Typography,
Link,
Popover,
IconButton,
} 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 { PublicKey } from "@solana/web3.js";
import { useTokenList } from "./context/TokenList";
import { useSwapContext } from "./context/Swap";
import { useMint } from "./context/Mint";
import { useMarketRoute } from "./context/Dex";
import {
useDexContext,
useMarketName,
useFair,
useFairRoute,
} from "./context/Dex";
const useStyles = makeStyles((theme) => ({
infoLabel: {
@ -23,6 +34,9 @@ const useStyles = makeStyles((theme) => ({
flexDirection: "column",
color: theme.palette.text.secondary,
},
infoButton: {
padding: 0,
},
}));
export function InfoLabel() {
@ -30,7 +44,7 @@ export function InfoLabel() {
const { fromMint, toMint } = useSwapContext();
const fromMintInfo = useMint(fromMint);
const fair = useFair(fromMint, toMint);
const fair = useFairRoute(fromMint, toMint);
const tokenList = useTokenList();
let fromTokenInfo = tokenList.filter(
@ -49,33 +63,26 @@ export function InfoLabel() {
)} ${fromTokenInfo.symbol}`
: `-`}
</div>
<InfoPopover />
<InfoButton />
</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 },
];
function InfoButton() {
const styles = useStyles();
return (
<PopupState variant="popover">
{
//@ts-ignore
(popupState) => (
<div style={{ display: "flex" }}>
<Info {...bindTrigger(popupState)} />
<IconButton
{...bindTrigger(popupState)}
className={styles.infoButton}
>
<Info />
</IconButton>
<Popover
{...bindPopover(popupState)}
anchorOrigin={{
@ -89,68 +96,7 @@ function InfoPopover() {
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>
<InfoDetails />
</Popover>
</div>
)
@ -158,3 +104,88 @@ function InfoPopover() {
</PopupState>
);
}
function InfoDetails() {
const { fromMint, toMint } = useSwapContext();
const { swapClient } = useDexContext();
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 (
<div style={{ padding: "15px", width: "250px" }}>
<div>
<Typography
color="textSecondary"
style={{ fontWeight: "bold", marginBottom: "5px" }}
>
Trade Route
</Typography>
{swapClient.route(fromMint, toMint).map((market: PublicKey) => {
return <MarketRoute key={market.toString()} market={market} />;
})}
</div>
<div style={{ marginTop: "15px" }}>
<Typography
color="textSecondary"
style={{ fontWeight: "bold", marginBottom: "5px" }}
>
Tokens
</Typography>
{addresses.map((address) => {
return (
<div
key={address.mint.toString()}
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>
);
}
function MarketRoute({ market }: { market: PublicKey }) {
const marketName = useMarketName(market);
const fair = useFair(market);
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: "5px",
}}
>
<Link
href={`https://dex.projectserum.com/#/market/${market.toString()}`}
target="_blank"
rel="noopener"
>
{marketName}
</Link>
<code style={{ marginLeft: "10px" }}>{fair ? fair.toFixed(6) : "-"}</code>
</div>
);
}

View File

@ -43,8 +43,6 @@ const useStyles = makeStyles(() => ({
export function SettingsButton() {
const styles = useStyles();
const { slippage, setSlippage } = useSwapContext();
const [showSettingsDialog, setShowSettingsDialog] = useState(false);
return (
<PopupState variant="popover">
@ -70,47 +68,7 @@ export function SettingsButton() {
}}
PaperProps={{ style: { borderRadius: "10px" } }}
>
<div style={{ padding: "15px", width: "305px" }}>
<Typography
color="textSecondary"
style={{ fontWeight: "bold" }}
>
Settings
</Typography>
<div style={{ marginTop: "10px" }}>
<Typography>Slippage tolerance</Typography>
<TextField
type="number"
placeholder="Error tolerance percentage"
value={slippage}
onChange={(e) => setSlippage(parseFloat(e.target.value))}
style={{
display: "flex",
justifyContent: "center",
flexDirection: "column",
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">%</InputAdornment>
),
}}
/>
<Button
style={{
width: "100%",
marginTop: "10px",
background: "#e0e0e0",
}}
onClick={() => setShowSettingsDialog(true)}
>
Manage Dex Accounts
</Button>
</div>
<SettingsDialog
open={showSettingsDialog}
onClose={() => setShowSettingsDialog(false)}
/>
</div>
<SettingsDetails />
</Popover>
</div>
)
@ -119,6 +77,49 @@ export function SettingsButton() {
);
}
function SettingsDetails() {
const { slippage, setSlippage } = useSwapContext();
const [showSettingsDialog, setShowSettingsDialog] = useState(false);
return (
<div style={{ padding: "15px", width: "305px" }}>
<Typography color="textSecondary" style={{ fontWeight: "bold" }}>
Settings
</Typography>
<div style={{ marginTop: "10px" }}>
<Typography>Slippage tolerance</Typography>
<TextField
type="number"
placeholder="Error tolerance percentage"
value={slippage}
onChange={(e) => setSlippage(parseFloat(e.target.value))}
style={{
display: "flex",
justifyContent: "center",
flexDirection: "column",
}}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>,
}}
/>
<Button
style={{
width: "100%",
marginTop: "10px",
background: "#e0e0e0",
}}
onClick={() => setShowSettingsDialog(true)}
>
Manage Dex Accounts
</Button>
</div>
<SettingsDialog
open={showSettingsDialog}
onClose={() => setShowSettingsDialog(false)}
/>
</div>
);
}
export function SettingsDialog({
open,
onClose,

View File

@ -63,8 +63,8 @@ export default function Swap({
<TokenListContextProvider tokenList={tokenList}>
<MintContextProvider provider={provider}>
<TokenContextProvider provider={provider}>
<DexContextProvider provider={provider}>
<SwapContextProvider swapClient={swapClient}>
<DexContextProvider swapClient={swapClient}>
<SwapContextProvider>
<SwapCard style={style} />
</SwapContextProvider>
</DexContextProvider>

View File

@ -11,11 +11,11 @@ import {
Typography,
} from "@material-ui/core";
import { TokenIcon } from "./Swap";
import { useSwapContext } from "./context/Swap";
import { useDexContext } from "./context/Dex";
import { useTokenList } from "./context/TokenList";
import { USDC_MINT, USDT_MINT } from "../utils/pubkeys";
const useStyles = makeStyles((theme) => ({
const useStyles = makeStyles(() => ({
dialogContent: {
paddingTop: 0,
},
@ -38,7 +38,7 @@ export default function TokenDialog({
}) {
const [tokenFilter, setTokenFilter] = useState("");
const styles = useStyles();
const { swapClient } = useSwapContext();
const { swapClient } = useDexContext();
return (
<Dialog
open={open}

View File

@ -1,7 +1,7 @@
import React, { useContext, useState, useEffect } from "react";
import React, { useContext, useState, useEffect, useMemo } 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,
@ -9,6 +9,7 @@ import {
} from "@project-serum/serum";
import { PublicKey } from "@solana/web3.js";
import { DEX_PID } from "../../utils/pubkeys";
import { useTokenList } from "./TokenList";
type DexContext = {
// Maps market address to open orders accounts.
@ -17,7 +18,7 @@ type DexContext = {
setMarketCache: (c: Map<string, Market>) => void;
orderbookCache: Map<string, Orderbook>;
setOrderbookCache: (c: Map<string, Orderbook>) => void;
provider: Provider;
swapClient: SwapClient;
};
const _DexContext = React.createContext<DexContext | null>(null);
@ -31,7 +32,7 @@ export function DexContextProvider(props: any) {
const [orderbookCache, setOrderbookCache] = useState<Map<string, Orderbook>>(
new Map()
);
const provider = props.provider;
const swapClient = props.swapClient;
// Two operations:
//
@ -40,8 +41,8 @@ export function DexContextProvider(props: any) {
//
useEffect(() => {
OpenOrders.findForOwner(
provider.connection,
provider.wallet.publicKey,
swapClient.program.provider.connection,
swapClient.program.provider.wallet.publicKey,
DEX_PID
).then(async (openOrders) => {
const newOoAccounts = new Map();
@ -62,7 +63,7 @@ export function DexContextProvider(props: any) {
}
const marketAccounts = (
await anchor.utils.getMultipleAccounts(
provider.connection,
swapClient.program.provider.connection,
// @ts-ignore
[...markets].map((m) => new PublicKey(m))
)
@ -73,7 +74,7 @@ export function DexContextProvider(props: any) {
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,
swapClient.program.provider.opts,
DEX_PID
),
};
@ -87,7 +88,11 @@ export function DexContextProvider(props: any) {
});
setOoAccounts(newOoAccounts);
});
}, [provider.connection, provider.wallet.publicKey, provider.opts]);
}, [
swapClient.program.provider.connection,
swapClient.program.provider.wallet.publicKey,
swapClient.program.provider.opts,
]);
return (
<_DexContext.Provider
value={{
@ -96,7 +101,7 @@ export function DexContextProvider(props: any) {
setMarketCache,
orderbookCache,
setOrderbookCache,
provider,
swapClient,
}}
>
{props.children}
@ -104,7 +109,7 @@ export function DexContextProvider(props: any) {
);
}
function useDexContext(): DexContext {
export function useDexContext(): DexContext {
const ctx = useContext(_DexContext);
if (ctx === null) {
throw new Error("Context not available");
@ -118,15 +123,18 @@ export function useOpenOrders(): Map<string, Array<OpenOrders>> {
}
// Lazy load a given market.
export function useMarket(market: PublicKey): Market | undefined {
export function useMarket(market?: PublicKey): Market | undefined {
const ctx = useDexContext();
const asyncMarket = useAsync(async () => {
if (!market) {
return undefined;
}
if (ctx.marketCache.get(market.toString())) {
return ctx.marketCache.get(market.toString());
}
const marketClient = await Market.load(
ctx.provider.connection,
ctx.swapClient.program.provider.connection,
market,
undefined,
DEX_PID
@ -137,7 +145,7 @@ export function useMarket(market: PublicKey): Market | undefined {
ctx.setMarketCache(cache);
return marketClient;
}, [ctx.provider.connection, market]);
}, [ctx.swapClient.program.provider.connection, market]);
if (asyncMarket.result) {
return asyncMarket.result;
@ -147,21 +155,21 @@ export function useMarket(market: PublicKey): Market | undefined {
}
// Lazy load the orderbook for a given market.
export function useOrderbook(market: PublicKey): Orderbook | undefined {
const ctx = useDexContext();
export function useOrderbook(market?: PublicKey): Orderbook | undefined {
const { swapClient, orderbookCache, setOrderbookCache } = useDexContext();
const marketClient = useMarket(market);
const asyncOrderbook = useAsync(async () => {
if (!marketClient) {
if (!market || !marketClient) {
return undefined;
}
if (ctx.orderbookCache.get(market.toString())) {
return ctx.orderbookCache.get(market.toString());
if (orderbookCache.get(market.toString())) {
return orderbookCache.get(market.toString());
}
const [bids, asks] = await Promise.all([
marketClient.loadBids(ctx.provider.connection),
marketClient.loadAsks(ctx.provider.connection),
marketClient.loadBids(swapClient.program.provider.connection),
marketClient.loadAsks(swapClient.program.provider.connection),
]);
const orderbook = {
@ -169,12 +177,12 @@ export function useOrderbook(market: PublicKey): Orderbook | undefined {
asks,
};
const cache = new Map(ctx.orderbookCache);
const cache = new Map(orderbookCache);
cache.set(market.toString(), orderbook);
ctx.setOrderbookCache(cache);
setOrderbookCache(cache);
return orderbook;
}, [ctx.provider.connection, market, marketClient]);
}, [swapClient.program.provider.connection, market, marketClient]);
if (asyncOrderbook.result) {
return asyncOrderbook.result;
@ -183,33 +191,53 @@ export function useOrderbook(market: PublicKey): Orderbook | undefined {
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,
},
];
export function useMarketName(market: PublicKey): string {
const tokenList = useTokenList();
const marketClient = useMarket(market);
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 name = `${baseTicker} / ${quoteTicker}`;
return name;
}
// Fair price for a given market, as defined by the mid.
export function useFair(market?: PublicKey): number | undefined {
const orderbook = useOrderbook(market);
if (orderbook === undefined) {
return undefined;
}
const bestBid = orderbook.bids.items(true).next().value;
const bestOffer = orderbook.asks.items(false).next().value;
const mid = (bestBid.price + bestOffer.price) / 2.0;
return mid;
}
// Fair price for a theoretical toMint/fromMint market. I.e., the number
// of `fromMint` tokens to purchase a single `toMint` token.
export function useFair(
// of `fromMint` tokens to purchase a single `toMint` token. Aggregates
// across a trade route, if needed.
export function useFairRoute(
fromMint: PublicKey,
toMint: PublicKey
): number | undefined {
// todo
return 0.5;
const { swapClient } = useDexContext();
const route = useMemo(
() => swapClient.route(fromMint, toMint),
[swapClient, fromMint, toMint]
);
const fromFair = useFair(route[0]);
const toFair = useFair(route[1]);
if (route.length === 1 && fromFair !== undefined) {
return 1.0 / fromFair;
}
if (fromFair === undefined || toFair === undefined) {
return undefined;
}
return toFair / fromFair;
}
type Orderbook = {

View File

@ -1,12 +1,12 @@
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";
import { useFairRoute } from "./Dex";
const DEFAULT_SLIPPAGE_PERCENT = 0.5;
export type SwapContext = {
swapClient: SwapClient;
fromMint: PublicKey;
setFromMint: (m: PublicKey) => void;
toMint: PublicKey;
@ -24,14 +24,13 @@ export type SwapContext = {
const _SwapContext = React.createContext<null | SwapContext>(null);
export function SwapContextProvider(props: any) {
const swapClient = props.swapClient;
const [fromMint, setFromMint] = useState(SRM_MINT);
const [toMint, setToMint] = useState(USDC_MINT);
const [fromAmount, _setFromAmount] = useState(0);
const [toAmount, _setToAmount] = useState(0);
// Percent units.
const [slippage, setSlippage] = useState(0.5);
const fair = useFair(fromMint, toMint);
const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE_PERCENT);
const fair = useFairRoute(fromMint, toMint);
const swapToFromMints = () => {
const oldFrom = fromMint;
@ -63,7 +62,6 @@ export function SwapContextProvider(props: any) {
return (
<_SwapContext.Provider
value={{
swapClient,
fromMint,
setFromMint,
toMint,

View File

@ -1577,15 +1577,15 @@
bs58 "^4.0.1"
eventemitter3 "^4.0.4"
"@project-serum/swap@^0.1.0-alpha.2":
version "0.1.0-alpha.2"
resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.2.tgz#dc4bb2a182163e47deae8f67f433837d6a09a498"
integrity sha512-p9kaae3WyOvL2Js1fskP59pzFm/tBNl8+mrD+obqvyWukUj8lnGIdn9083iyGdSD8GWMrP3dmYQqngfHOiIbVg==
"@project-serum/swap@^0.1.0-alpha.3":
version "0.1.0-alpha.3"
resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.3.tgz#f400b646b2c40f41d34a4a273054be1051576296"
integrity sha512-pjk+uo2llyOhJnf7NCXkunm8dPlDOUbDWx97Xq/R7G/qDckBgbXuJxTE+5w0kxr6f2FwecglHDpfnfwXFJdFKQ==
dependencies:
"@project-serum/anchor" "^0.5.1-beta.2"
"@project-serum/serum" "^0.13.34"
"@solana/spl-token" "^0.1.3"
"@solana/spl-token-registry" "^0.2.68"
"@solana/spl-token-registry" "^0.2.86"
"@solana/web3.js" "^1.2.0"
base64-js "^1.5.1"
bn.js "^5.1.2"
@ -1632,7 +1632,7 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@solana/spl-token-registry@^0.2.68", "@solana/spl-token-registry@^0.2.86":
"@solana/spl-token-registry@^0.2.86":
version "0.2.86"
resolved "https://registry.yarnpkg.com/@solana/spl-token-registry/-/spl-token-registry-0.2.86.tgz#6ca8172d0f0c38ffc3dae174e3ba72674fd2ad66"
integrity sha512-/ySaKRMRmCSHxiWonlFn+07Mj3wze6zpB6RvY169GQAiO+6RGDZPUP5kbdSHP9NXAInG9LyKjd6rEU5ymFfG3A==