Trade route fairs
This commit is contained in:
parent
dc5abad2c0
commit
3ad62023c8
|
@ -6,9 +6,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.4",
|
"@material-ui/core": "^4.11.4",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
|
"@project-serum/anchor": "^0.5.1-beta.2",
|
||||||
"@project-serum/serum": "^0.13.34",
|
"@project-serum/serum": "^0.13.34",
|
||||||
"@project-serum/sol-wallet-adapter": "^0.2.0",
|
"@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": "^0.1.4",
|
||||||
"@solana/spl-token-registry": "^0.2.86",
|
"@solana/spl-token-registry": "^0.2.86",
|
||||||
"@solana/web3.js": "^1.10.1",
|
"@solana/web3.js": "^1.10.1",
|
||||||
|
|
|
@ -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 { Info } from "@material-ui/icons";
|
||||||
import PopupState, { bindTrigger, bindPopover } from "material-ui-popup-state";
|
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 { useTokenList } from "./context/TokenList";
|
||||||
import { useSwapContext } from "./context/Swap";
|
import { useSwapContext } from "./context/Swap";
|
||||||
import { useMint } from "./context/Mint";
|
import { useMint } from "./context/Mint";
|
||||||
import { useMarketRoute } from "./context/Dex";
|
import {
|
||||||
|
useDexContext,
|
||||||
|
useMarketName,
|
||||||
|
useFair,
|
||||||
|
useFairRoute,
|
||||||
|
} from "./context/Dex";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
infoLabel: {
|
infoLabel: {
|
||||||
|
@ -23,6 +34,9 @@ const useStyles = makeStyles((theme) => ({
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
},
|
},
|
||||||
|
infoButton: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function InfoLabel() {
|
export function InfoLabel() {
|
||||||
|
@ -30,7 +44,7 @@ export function InfoLabel() {
|
||||||
|
|
||||||
const { fromMint, toMint } = useSwapContext();
|
const { fromMint, toMint } = useSwapContext();
|
||||||
const fromMintInfo = useMint(fromMint);
|
const fromMintInfo = useMint(fromMint);
|
||||||
const fair = useFair(fromMint, toMint);
|
const fair = useFairRoute(fromMint, toMint);
|
||||||
|
|
||||||
const tokenList = useTokenList();
|
const tokenList = useTokenList();
|
||||||
let fromTokenInfo = tokenList.filter(
|
let fromTokenInfo = tokenList.filter(
|
||||||
|
@ -49,33 +63,26 @@ export function InfoLabel() {
|
||||||
)} ${fromTokenInfo.symbol}`
|
)} ${fromTokenInfo.symbol}`
|
||||||
: `-`}
|
: `-`}
|
||||||
</div>
|
</div>
|
||||||
<InfoPopover />
|
<InfoButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoPopover() {
|
function InfoButton() {
|
||||||
const { fromMint, toMint } = useSwapContext();
|
const styles = useStyles();
|
||||||
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 (
|
return (
|
||||||
<PopupState variant="popover">
|
<PopupState variant="popover">
|
||||||
{
|
{
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
(popupState) => (
|
(popupState) => (
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
<Info {...bindTrigger(popupState)} />
|
<IconButton
|
||||||
|
{...bindTrigger(popupState)}
|
||||||
|
className={styles.infoButton}
|
||||||
|
>
|
||||||
|
<Info />
|
||||||
|
</IconButton>
|
||||||
<Popover
|
<Popover
|
||||||
{...bindPopover(popupState)}
|
{...bindPopover(popupState)}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
|
@ -89,68 +96,7 @@ function InfoPopover() {
|
||||||
PaperProps={{ style: { borderRadius: "10px" } }}
|
PaperProps={{ style: { borderRadius: "10px" } }}
|
||||||
disableRestoreFocus
|
disableRestoreFocus
|
||||||
>
|
>
|
||||||
<div style={{ padding: "15px", width: "250px" }}>
|
<InfoDetails />
|
||||||
<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>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -158,3 +104,88 @@ function InfoPopover() {
|
||||||
</PopupState>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -43,8 +43,6 @@ const useStyles = makeStyles(() => ({
|
||||||
|
|
||||||
export function SettingsButton() {
|
export function SettingsButton() {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { slippage, setSlippage } = useSwapContext();
|
|
||||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopupState variant="popover">
|
<PopupState variant="popover">
|
||||||
|
@ -70,47 +68,7 @@ export function SettingsButton() {
|
||||||
}}
|
}}
|
||||||
PaperProps={{ style: { borderRadius: "10px" } }}
|
PaperProps={{ style: { borderRadius: "10px" } }}
|
||||||
>
|
>
|
||||||
<div style={{ padding: "15px", width: "305px" }}>
|
<SettingsDetails />
|
||||||
<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>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</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({
|
export function SettingsDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
|
@ -63,8 +63,8 @@ export default function Swap({
|
||||||
<TokenListContextProvider tokenList={tokenList}>
|
<TokenListContextProvider tokenList={tokenList}>
|
||||||
<MintContextProvider provider={provider}>
|
<MintContextProvider provider={provider}>
|
||||||
<TokenContextProvider provider={provider}>
|
<TokenContextProvider provider={provider}>
|
||||||
<DexContextProvider provider={provider}>
|
<DexContextProvider swapClient={swapClient}>
|
||||||
<SwapContextProvider swapClient={swapClient}>
|
<SwapContextProvider>
|
||||||
<SwapCard style={style} />
|
<SwapCard style={style} />
|
||||||
</SwapContextProvider>
|
</SwapContextProvider>
|
||||||
</DexContextProvider>
|
</DexContextProvider>
|
||||||
|
|
|
@ -11,11 +11,11 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { TokenIcon } from "./Swap";
|
import { TokenIcon } from "./Swap";
|
||||||
import { useSwapContext } from "./context/Swap";
|
import { useDexContext } from "./context/Dex";
|
||||||
import { useTokenList } from "./context/TokenList";
|
import { useTokenList } from "./context/TokenList";
|
||||||
import { USDC_MINT, USDT_MINT } from "../utils/pubkeys";
|
import { USDC_MINT, USDT_MINT } from "../utils/pubkeys";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles(() => ({
|
||||||
dialogContent: {
|
dialogContent: {
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
},
|
},
|
||||||
|
@ -38,7 +38,7 @@ export default function TokenDialog({
|
||||||
}) {
|
}) {
|
||||||
const [tokenFilter, setTokenFilter] = useState("");
|
const [tokenFilter, setTokenFilter] = useState("");
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { swapClient } = useSwapContext();
|
const { swapClient } = useDexContext();
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
|
|
|
@ -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 { useAsync } from "react-async-hook";
|
||||||
import * as anchor from "@project-serum/anchor";
|
import * as anchor from "@project-serum/anchor";
|
||||||
import { Provider } from "@project-serum/anchor";
|
import { Swap as SwapClient } from "@project-serum/swap";
|
||||||
import {
|
import {
|
||||||
Market,
|
Market,
|
||||||
OpenOrders,
|
OpenOrders,
|
||||||
|
@ -9,6 +9,7 @@ import {
|
||||||
} from "@project-serum/serum";
|
} from "@project-serum/serum";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import { DEX_PID } from "../../utils/pubkeys";
|
import { DEX_PID } from "../../utils/pubkeys";
|
||||||
|
import { useTokenList } from "./TokenList";
|
||||||
|
|
||||||
type DexContext = {
|
type DexContext = {
|
||||||
// Maps market address to open orders accounts.
|
// Maps market address to open orders accounts.
|
||||||
|
@ -17,7 +18,7 @@ type DexContext = {
|
||||||
setMarketCache: (c: Map<string, Market>) => void;
|
setMarketCache: (c: Map<string, Market>) => void;
|
||||||
orderbookCache: Map<string, Orderbook>;
|
orderbookCache: Map<string, Orderbook>;
|
||||||
setOrderbookCache: (c: Map<string, Orderbook>) => void;
|
setOrderbookCache: (c: Map<string, Orderbook>) => void;
|
||||||
provider: Provider;
|
swapClient: SwapClient;
|
||||||
};
|
};
|
||||||
const _DexContext = React.createContext<DexContext | null>(null);
|
const _DexContext = React.createContext<DexContext | null>(null);
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ export function DexContextProvider(props: any) {
|
||||||
const [orderbookCache, setOrderbookCache] = useState<Map<string, Orderbook>>(
|
const [orderbookCache, setOrderbookCache] = useState<Map<string, Orderbook>>(
|
||||||
new Map()
|
new Map()
|
||||||
);
|
);
|
||||||
const provider = props.provider;
|
const swapClient = props.swapClient;
|
||||||
|
|
||||||
// Two operations:
|
// Two operations:
|
||||||
//
|
//
|
||||||
|
@ -40,8 +41,8 @@ export function DexContextProvider(props: any) {
|
||||||
//
|
//
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
OpenOrders.findForOwner(
|
OpenOrders.findForOwner(
|
||||||
provider.connection,
|
swapClient.program.provider.connection,
|
||||||
provider.wallet.publicKey,
|
swapClient.program.provider.wallet.publicKey,
|
||||||
DEX_PID
|
DEX_PID
|
||||||
).then(async (openOrders) => {
|
).then(async (openOrders) => {
|
||||||
const newOoAccounts = new Map();
|
const newOoAccounts = new Map();
|
||||||
|
@ -62,7 +63,7 @@ export function DexContextProvider(props: any) {
|
||||||
}
|
}
|
||||||
const marketAccounts = (
|
const marketAccounts = (
|
||||||
await anchor.utils.getMultipleAccounts(
|
await anchor.utils.getMultipleAccounts(
|
||||||
provider.connection,
|
swapClient.program.provider.connection,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
[...markets].map((m) => new PublicKey(m))
|
[...markets].map((m) => new PublicKey(m))
|
||||||
)
|
)
|
||||||
|
@ -73,7 +74,7 @@ export function DexContextProvider(props: any) {
|
||||||
Market.getLayout(DEX_PID).decode(programAccount?.account.data),
|
Market.getLayout(DEX_PID).decode(programAccount?.account.data),
|
||||||
-1, // Not used so don't bother fetching.
|
-1, // Not used so don't bother fetching.
|
||||||
-1, // Not used so don't bother fetching.
|
-1, // Not used so don't bother fetching.
|
||||||
provider.opts,
|
swapClient.program.provider.opts,
|
||||||
DEX_PID
|
DEX_PID
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -87,7 +88,11 @@ export function DexContextProvider(props: any) {
|
||||||
});
|
});
|
||||||
setOoAccounts(newOoAccounts);
|
setOoAccounts(newOoAccounts);
|
||||||
});
|
});
|
||||||
}, [provider.connection, provider.wallet.publicKey, provider.opts]);
|
}, [
|
||||||
|
swapClient.program.provider.connection,
|
||||||
|
swapClient.program.provider.wallet.publicKey,
|
||||||
|
swapClient.program.provider.opts,
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<_DexContext.Provider
|
<_DexContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -96,7 +101,7 @@ export function DexContextProvider(props: any) {
|
||||||
setMarketCache,
|
setMarketCache,
|
||||||
orderbookCache,
|
orderbookCache,
|
||||||
setOrderbookCache,
|
setOrderbookCache,
|
||||||
provider,
|
swapClient,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -104,7 +109,7 @@ export function DexContextProvider(props: any) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDexContext(): DexContext {
|
export function useDexContext(): DexContext {
|
||||||
const ctx = useContext(_DexContext);
|
const ctx = useContext(_DexContext);
|
||||||
if (ctx === null) {
|
if (ctx === null) {
|
||||||
throw new Error("Context not available");
|
throw new Error("Context not available");
|
||||||
|
@ -118,15 +123,18 @@ export function useOpenOrders(): Map<string, Array<OpenOrders>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy load a given market.
|
// Lazy load a given market.
|
||||||
export function useMarket(market: PublicKey): Market | undefined {
|
export function useMarket(market?: PublicKey): Market | undefined {
|
||||||
const ctx = useDexContext();
|
const ctx = useDexContext();
|
||||||
|
|
||||||
const asyncMarket = useAsync(async () => {
|
const asyncMarket = useAsync(async () => {
|
||||||
|
if (!market) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (ctx.marketCache.get(market.toString())) {
|
if (ctx.marketCache.get(market.toString())) {
|
||||||
return ctx.marketCache.get(market.toString());
|
return ctx.marketCache.get(market.toString());
|
||||||
}
|
}
|
||||||
const marketClient = await Market.load(
|
const marketClient = await Market.load(
|
||||||
ctx.provider.connection,
|
ctx.swapClient.program.provider.connection,
|
||||||
market,
|
market,
|
||||||
undefined,
|
undefined,
|
||||||
DEX_PID
|
DEX_PID
|
||||||
|
@ -137,7 +145,7 @@ export function useMarket(market: PublicKey): Market | undefined {
|
||||||
ctx.setMarketCache(cache);
|
ctx.setMarketCache(cache);
|
||||||
|
|
||||||
return marketClient;
|
return marketClient;
|
||||||
}, [ctx.provider.connection, market]);
|
}, [ctx.swapClient.program.provider.connection, market]);
|
||||||
|
|
||||||
if (asyncMarket.result) {
|
if (asyncMarket.result) {
|
||||||
return asyncMarket.result;
|
return asyncMarket.result;
|
||||||
|
@ -147,21 +155,21 @@ export function useMarket(market: PublicKey): Market | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy load the orderbook for a given market.
|
// Lazy load the orderbook for a given market.
|
||||||
export function useOrderbook(market: PublicKey): Orderbook | undefined {
|
export function useOrderbook(market?: PublicKey): Orderbook | undefined {
|
||||||
const ctx = useDexContext();
|
const { swapClient, orderbookCache, setOrderbookCache } = useDexContext();
|
||||||
const marketClient = useMarket(market);
|
const marketClient = useMarket(market);
|
||||||
|
|
||||||
const asyncOrderbook = useAsync(async () => {
|
const asyncOrderbook = useAsync(async () => {
|
||||||
if (!marketClient) {
|
if (!market || !marketClient) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (ctx.orderbookCache.get(market.toString())) {
|
if (orderbookCache.get(market.toString())) {
|
||||||
return ctx.orderbookCache.get(market.toString());
|
return orderbookCache.get(market.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const [bids, asks] = await Promise.all([
|
const [bids, asks] = await Promise.all([
|
||||||
marketClient.loadBids(ctx.provider.connection),
|
marketClient.loadBids(swapClient.program.provider.connection),
|
||||||
marketClient.loadAsks(ctx.provider.connection),
|
marketClient.loadAsks(swapClient.program.provider.connection),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const orderbook = {
|
const orderbook = {
|
||||||
|
@ -169,12 +177,12 @@ export function useOrderbook(market: PublicKey): Orderbook | undefined {
|
||||||
asks,
|
asks,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cache = new Map(ctx.orderbookCache);
|
const cache = new Map(orderbookCache);
|
||||||
cache.set(market.toString(), orderbook);
|
cache.set(market.toString(), orderbook);
|
||||||
ctx.setOrderbookCache(cache);
|
setOrderbookCache(cache);
|
||||||
|
|
||||||
return orderbook;
|
return orderbook;
|
||||||
}, [ctx.provider.connection, market, marketClient]);
|
}, [swapClient.program.provider.connection, market, marketClient]);
|
||||||
|
|
||||||
if (asyncOrderbook.result) {
|
if (asyncOrderbook.result) {
|
||||||
return asyncOrderbook.result;
|
return asyncOrderbook.result;
|
||||||
|
@ -183,33 +191,53 @@ export function useOrderbook(market: PublicKey): Orderbook | undefined {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMarketRoute(
|
export function useMarketName(market: PublicKey): string {
|
||||||
fromMint: PublicKey,
|
const tokenList = useTokenList();
|
||||||
toMint: PublicKey
|
const marketClient = useMarket(market);
|
||||||
): Array<{ address: PublicKey; name: string; fair: number }> {
|
const baseTicker = tokenList
|
||||||
// todo
|
.filter((t) => t.address === marketClient?.baseMintAddress.toString())
|
||||||
return [
|
.map((t) => t.symbol)[0];
|
||||||
{
|
const quoteTicker = tokenList
|
||||||
address: new PublicKey("ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA"),
|
.filter((t) => t.address === marketClient?.quoteMintAddress.toString())
|
||||||
name: "SRM / USDC",
|
.map((t) => t.symbol)[0];
|
||||||
fair: 0.5,
|
const name = `${baseTicker} / ${quoteTicker}`;
|
||||||
},
|
return name;
|
||||||
{
|
}
|
||||||
address: new PublicKey("J7cPYBrXVy8Qeki2crZkZavcojf2sMRyQU7nx438Mf8t"),
|
|
||||||
name: "MATH / USDC",
|
// Fair price for a given market, as defined by the mid.
|
||||||
fair: 1.23,
|
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
|
// Fair price for a theoretical toMint/fromMint market. I.e., the number
|
||||||
// of `fromMint` tokens to purchase a single `toMint` token.
|
// of `fromMint` tokens to purchase a single `toMint` token. Aggregates
|
||||||
export function useFair(
|
// across a trade route, if needed.
|
||||||
|
export function useFairRoute(
|
||||||
fromMint: PublicKey,
|
fromMint: PublicKey,
|
||||||
toMint: PublicKey
|
toMint: PublicKey
|
||||||
): number | undefined {
|
): number | undefined {
|
||||||
// todo
|
const { swapClient } = useDexContext();
|
||||||
return 0.5;
|
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 = {
|
type Orderbook = {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { Swap as SwapClient } from "@project-serum/swap";
|
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import { MintInfo } from "@solana/spl-token";
|
import { MintInfo } from "@solana/spl-token";
|
||||||
import { SRM_MINT, USDC_MINT } from "../../utils/pubkeys";
|
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 = {
|
export type SwapContext = {
|
||||||
swapClient: SwapClient;
|
|
||||||
fromMint: PublicKey;
|
fromMint: PublicKey;
|
||||||
setFromMint: (m: PublicKey) => void;
|
setFromMint: (m: PublicKey) => void;
|
||||||
toMint: PublicKey;
|
toMint: PublicKey;
|
||||||
|
@ -24,14 +24,13 @@ export type SwapContext = {
|
||||||
const _SwapContext = React.createContext<null | SwapContext>(null);
|
const _SwapContext = React.createContext<null | SwapContext>(null);
|
||||||
|
|
||||||
export function SwapContextProvider(props: any) {
|
export function SwapContextProvider(props: any) {
|
||||||
const swapClient = props.swapClient;
|
|
||||||
const [fromMint, setFromMint] = useState(SRM_MINT);
|
const [fromMint, setFromMint] = useState(SRM_MINT);
|
||||||
const [toMint, setToMint] = useState(USDC_MINT);
|
const [toMint, setToMint] = useState(USDC_MINT);
|
||||||
const [fromAmount, _setFromAmount] = useState(0);
|
const [fromAmount, _setFromAmount] = useState(0);
|
||||||
const [toAmount, _setToAmount] = useState(0);
|
const [toAmount, _setToAmount] = useState(0);
|
||||||
// Percent units.
|
// Percent units.
|
||||||
const [slippage, setSlippage] = useState(0.5);
|
const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE_PERCENT);
|
||||||
const fair = useFair(fromMint, toMint);
|
const fair = useFairRoute(fromMint, toMint);
|
||||||
|
|
||||||
const swapToFromMints = () => {
|
const swapToFromMints = () => {
|
||||||
const oldFrom = fromMint;
|
const oldFrom = fromMint;
|
||||||
|
@ -63,7 +62,6 @@ export function SwapContextProvider(props: any) {
|
||||||
return (
|
return (
|
||||||
<_SwapContext.Provider
|
<_SwapContext.Provider
|
||||||
value={{
|
value={{
|
||||||
swapClient,
|
|
||||||
fromMint,
|
fromMint,
|
||||||
setFromMint,
|
setFromMint,
|
||||||
toMint,
|
toMint,
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -1577,15 +1577,15 @@
|
||||||
bs58 "^4.0.1"
|
bs58 "^4.0.1"
|
||||||
eventemitter3 "^4.0.4"
|
eventemitter3 "^4.0.4"
|
||||||
|
|
||||||
"@project-serum/swap@^0.1.0-alpha.2":
|
"@project-serum/swap@^0.1.0-alpha.3":
|
||||||
version "0.1.0-alpha.2"
|
version "0.1.0-alpha.3"
|
||||||
resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.2.tgz#dc4bb2a182163e47deae8f67f433837d6a09a498"
|
resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.3.tgz#f400b646b2c40f41d34a4a273054be1051576296"
|
||||||
integrity sha512-p9kaae3WyOvL2Js1fskP59pzFm/tBNl8+mrD+obqvyWukUj8lnGIdn9083iyGdSD8GWMrP3dmYQqngfHOiIbVg==
|
integrity sha512-pjk+uo2llyOhJnf7NCXkunm8dPlDOUbDWx97Xq/R7G/qDckBgbXuJxTE+5w0kxr6f2FwecglHDpfnfwXFJdFKQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@project-serum/anchor" "^0.5.1-beta.2"
|
"@project-serum/anchor" "^0.5.1-beta.2"
|
||||||
"@project-serum/serum" "^0.13.34"
|
"@project-serum/serum" "^0.13.34"
|
||||||
"@solana/spl-token" "^0.1.3"
|
"@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"
|
"@solana/web3.js" "^1.2.0"
|
||||||
base64-js "^1.5.1"
|
base64-js "^1.5.1"
|
||||||
bn.js "^5.1.2"
|
bn.js "^5.1.2"
|
||||||
|
@ -1632,7 +1632,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sinonjs/commons" "^1.7.0"
|
"@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"
|
version "0.2.86"
|
||||||
resolved "https://registry.yarnpkg.com/@solana/spl-token-registry/-/spl-token-registry-0.2.86.tgz#6ca8172d0f0c38ffc3dae174e3ba72674fd2ad66"
|
resolved "https://registry.yarnpkg.com/@solana/spl-token-registry/-/spl-token-registry-0.2.86.tgz#6ca8172d0f0c38ffc3dae174e3ba72674fd2ad66"
|
||||||
integrity sha512-/ySaKRMRmCSHxiWonlFn+07Mj3wze6zpB6RvY169GQAiO+6RGDZPUP5kbdSHP9NXAInG9LyKjd6rEU5ymFfG3A==
|
integrity sha512-/ySaKRMRmCSHxiWonlFn+07Mj3wze6zpB6RvY169GQAiO+6RGDZPUP5kbdSHP9NXAInG9LyKjd6rEU5ymFfG3A==
|
||||||
|
|
Loading…
Reference in New Issue