diff --git a/package.json b/package.json index 7955b21..81ae359 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dependencies": { "react-async-hook": "^3.6.2", "@project-serum/serum": "^0.13.34", - "@project-serum/swap": "^0.1.0-alpha.9", + "@project-serum/swap": "^0.1.0-alpha.11", "@solana/spl-token": "^0.1.4" }, "peerDependencies": { diff --git a/src/swap/components/Info.tsx b/src/swap/components/Info.tsx index ffbbe60..01d4843 100644 --- a/src/swap/components/Info.tsx +++ b/src/swap/components/Info.tsx @@ -11,7 +11,7 @@ import { PublicKey } from "@solana/web3.js"; import { useTokenMap } from "../context/TokenList"; import { useSwapContext, useSwapFair } from "../context/Swap"; import { useMint } from "../context/Token"; -import { useRoute, useMarketName, useFair } from "../context/Dex"; +import { useRoute, useMarketName, useBbo } from "../context/Dex"; const useStyles = makeStyles((theme) => ({ infoLabel: { @@ -163,7 +163,7 @@ function InfoDetails() { function MarketRoute({ market }: { market: PublicKey }) { const marketName = useMarketName(market); - const fair = useFair(market); + const bbo = useBbo(market); return (
{marketName} - {fair ? fair.toFixed(6) : "-"} + + {bbo && bbo.mid ? bbo.mid.toFixed(6) : "-"} +
); } diff --git a/src/swap/components/Settings.tsx b/src/swap/components/Settings.tsx index 70a7e72..eb6cdf9 100644 --- a/src/swap/components/Settings.tsx +++ b/src/swap/components/Settings.tsx @@ -73,6 +73,11 @@ function SettingsDetails() { const [showSettingsDialog, setShowSettingsDialog] = useState(false); const fair = useSwapFair(); const { swapClient } = useDexContext(); + + const setSlippageHandler = (value?: number) => { + setSlippage(!value || value < 0 ? 0 : value); + }; + return (
@@ -85,7 +90,7 @@ function SettingsDetails() { type="number" placeholder="Error tolerance percentage" value={slippage} - onChange={(e) => setSlippage(parseFloat(e.target.value))} + onChange={(e) => setSlippageHandler(parseFloat(e.target.value))} style={{ display: "flex", justifyContent: "center", diff --git a/src/swap/components/Swap.tsx b/src/swap/components/Swap.tsx index b1fca77..87dbfcd 100644 --- a/src/swap/components/Swap.tsx +++ b/src/swap/components/Swap.tsx @@ -10,12 +10,13 @@ import { TextField, } from "@material-ui/core"; import { ExpandMore } from "@material-ui/icons"; -import { useSwapContext } from "../context/Swap"; +import { useSwapContext, useSwapFair } from "../context/Swap"; import { useDexContext, useOpenOrders, useRouteVerbose, useMarket, + BASE_TAKER_FEE_BPS, } from "../context/Dex"; import { useTokenMap } from "../context/TokenList"; import { useMint, useOwnedTokenAccount } from "../context/Token"; @@ -229,14 +230,8 @@ function TokenName({ mint }: { mint: PublicKey }) { function SwapButton() { const styles = useStyles(); - const { - fromMint, - toMint, - fromAmount, - toAmount, - slippage, - isClosingNewAccounts, - } = useSwapContext(); + const { fromMint, toMint, fromAmount, slippage, isClosingNewAccounts } = + useSwapContext(); const { swapClient } = useDexContext(); const fromMintInfo = useMint(fromMint); const toMintInfo = useMint(toMint); @@ -250,19 +245,26 @@ function SwapButton() { ); const canSwap = useCanSwap(); const referral = useReferral(fromMarket); + const fair = useSwapFair(); // Click handler. const sendSwapTransaction = async () => { if (!fromMintInfo || !toMintInfo) { throw new Error("Unable to calculate mint decimals"); } + if (!fair) { + throw new Error("Invalid fair"); + } const amount = new BN(fromAmount).mul( new BN(10).pow(new BN(fromMintInfo.decimals)) ); - const minExpectedSwapAmount = new BN(toAmount) - .mul(new BN(10).pow(new BN(toMintInfo.decimals))) - .muln(100 - slippage) - .divn(100); + const minExchangeRate = { + rate: new BN(10 ** toMintInfo.decimals * (1 - BASE_TAKER_FEE_BPS)) + .divn(fair) + .muln(100 - slippage) + .divn(100), + decimals: fromMintInfo.decimals, + }; const fromOpenOrders = fromMarket ? openOrders.get(fromMarket?.address.toString()) : undefined; @@ -273,7 +275,7 @@ function SwapButton() { fromMint, toMint, amount, - minExpectedSwapAmount, + minExchangeRate, referral, // Pass in the below parameters so that the client doesn't perform // wasteful network requests when we already have the data. diff --git a/src/swap/context/Dex.tsx b/src/swap/context/Dex.tsx index b2a53c7..bbde5d2 100644 --- a/src/swap/context/Dex.tsx +++ b/src/swap/context/Dex.tsx @@ -22,6 +22,8 @@ import { import { useTokenMap, useTokenListContext } from "./TokenList"; import { fetchSolletInfo, requestWormholeSwapMarketIfNeeded } from "./Sollet"; +export const BASE_TAKER_FEE_BPS = 0.0022; + type DexContext = { // Maps market address to open orders accounts. openOrders: Map>; @@ -293,7 +295,7 @@ export function useMarketName(market: PublicKey): string | null { } // Fair price for a given market, as defined by the mid. -export function useFair(market?: PublicKey): number | undefined { +export function useBbo(market?: PublicKey): Bbo | undefined { const orderbook = useOrderbook(market); if (orderbook === undefined) { return undefined; @@ -301,13 +303,13 @@ export function useFair(market?: PublicKey): number | undefined { const bestBid = orderbook.bids.items(true).next().value; const bestOffer = orderbook.asks.items(false).next().value; if (!bestBid) { - return bestOffer.price; + return { bestOffer: bestOffer.price }; } if (!bestOffer) { - return bestBid.price; + return { bestBid: bestBid.price }; } const mid = (bestBid.price + bestOffer.price) / 2.0; - return mid; + return { bestBid: bestBid.price, bestOffer: bestOffer.price, mid }; } // Fair price for a theoretical toMint/fromMint market. I.e., the number @@ -318,28 +320,33 @@ export function useFairRoute( toMint: PublicKey ): number | undefined { const route = useRoute(fromMint, toMint); - const fromFair = useFair(route ? route[0] : undefined); + const fromBbo = useBbo(route ? route[0] : undefined); const fromMarket = useMarket(route ? route[0] : undefined); - const toFair = useFair(route ? route[1] : undefined); + const toBbo = useBbo(route ? route[1] : undefined); if (route === null) { return undefined; } - if (route.length === 1 && fromFair !== undefined) { + if (route.length === 1 && fromBbo !== undefined) { if (fromMarket === undefined) { return undefined; } if (fromMarket?.baseMintAddress.equals(fromMint)) { - return 1.0 / fromFair; + return fromBbo.bestBid && 1.0 / fromBbo.bestBid; } else { - return fromFair; + return fromBbo.bestOffer && fromBbo.bestOffer; } } - if (fromFair === undefined || toFair === undefined) { + if ( + fromBbo === undefined || + fromBbo.bestBid === undefined || + toBbo === undefined || + toBbo.bestOffer === undefined + ) { return undefined; } - return toFair / fromFair; + return toBbo.bestOffer / fromBbo.bestBid; } export function useRoute( @@ -525,3 +532,9 @@ async function deriveWormholeMarket( padToTwo(version); return await PublicKey.createWithSeed(WORM_MARKET_BASE, seed, DEX_PID); } + +type Bbo = { + bestBid?: number; + bestOffer?: number; + mid?: number; +}; diff --git a/yarn.lock b/yarn.lock index b3c0240..fb7f4b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1588,10 +1588,10 @@ bs58 "^4.0.1" eventemitter3 "^4.0.4" -"@project-serum/swap@^0.1.0-alpha.9": - version "0.1.0-alpha.9" - resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.9.tgz#bd3b7a48426a9dbeaea457375b9ffe9b6d0272ee" - integrity sha512-jrKpot+WPNBPTNcDoPD09XLFkr68wDVtob3vVx1eEcgN2xHtfjg0EKh42BM2BYasPsi7VSmS4yGwXui8j84jBw== +"@project-serum/swap@^0.1.0-alpha.11": + version "0.1.0-alpha.11" + resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.11.tgz#65c6393bdc511208453c853095eba32e0748191b" + integrity sha512-cxjxweWrZWcxfazkxSjDXqLquKlCCOoJ7z+cfAky/l29gRet+rLbGjyd+TAcZRDrIfh+kut9svPlL/bXK9vIOw== dependencies: "@project-serum/anchor" "^0.5.1-beta.2" "@project-serum/serum" "^0.13.34"