From 1fbad5c50ba2897776f744c2ee2d04c0bdde98ab Mon Sep 17 00:00:00 2001 From: broody Date: Mon, 17 Jul 2023 12:24:43 -0700 Subject: [PATCH] fix(web): max buy failure and slippage indicator --- web/src/components/icons/Alert.tsx | 13 ++++ web/src/components/icons/index.tsx | 1 + .../[gameId]/[locationSlug]/[drugSlug].tsx | 77 +++++++++++-------- web/src/utils/defi.ts | 30 ++++++++ 4 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 web/src/components/icons/Alert.tsx create mode 100644 web/src/utils/defi.ts diff --git a/web/src/components/icons/Alert.tsx b/web/src/components/icons/Alert.tsx new file mode 100644 index 00000000..78edd3ce --- /dev/null +++ b/web/src/components/icons/Alert.tsx @@ -0,0 +1,13 @@ +import { Icon, IconProps } from "."; + +export const Alert = (props: IconProps) => { + return ( + + + + ); +}; diff --git a/web/src/components/icons/index.tsx b/web/src/components/icons/index.tsx index dfd9691a..b37ffdba 100644 --- a/web/src/components/icons/index.tsx +++ b/web/src/components/icons/index.tsx @@ -38,6 +38,7 @@ export * from "./Trophy"; export * from "./User"; export * from "./Warning"; export * from "./Cart"; +export * from "./Alert"; // Template for adding new icons. When copying svg from figma, viewBox is assumed // to be 36x36, otherwise override within individual icons. diff --git a/web/src/pages/[gameId]/[locationSlug]/[drugSlug].tsx b/web/src/pages/[gameId]/[locationSlug]/[drugSlug].tsx index 78047933..d1368650 100644 --- a/web/src/pages/[gameId]/[locationSlug]/[drugSlug].tsx +++ b/web/src/pages/[gameId]/[locationSlug]/[drugSlug].tsx @@ -14,7 +14,7 @@ import Button from "@/components/Button"; import Layout from "@/components/Layout"; import { useRouter } from "next/router"; import { Footer } from "@/components/Footer"; -import { ArrowEnclosed, Cart } from "@/components/icons"; +import { Alert, ArrowEnclosed, Cart } from "@/components/icons"; import Image from "next/image"; import { DrugProps, getDrugBySlug, getLocationBySlug } from "@/hooks/ui"; @@ -48,6 +48,7 @@ import { } from "@/hooks/dojo/entities/usePlayerEntity"; import { formatQuantity, formatCash } from "@/utils/ui"; import { useRyoSystems } from "@/hooks/dojo/systems/useRyoSystems"; +import { calculateMaxQuantity, calculateSlippage } from "@/utils/defi"; export default function Market() { const router = useRouter(); @@ -64,8 +65,6 @@ export default function Market() { const [canSell, setCanSell] = useState(false); const [canBuy, setCanBuy] = useState(false); - const isBagFull = false; - const { location: locationEntity } = useLocationEntity({ gameId, locationName: location.name, @@ -173,27 +172,22 @@ export default function Market() { - {!isBagFull && ( - - )} + {!canBuy && } - {isBagFull && } {canSell ? ( @@ -237,36 +231,53 @@ const QuantitySelector = ({ type, player, drug, - marketPrice, - marketQuantity, + market, onChange, }: { type: TradeDirection; drug: DrugProps; player: PlayerEntity; - marketPrice: number; - marketQuantity: number; + market: DrugMarket; onChange: (quantity: number) => void; }) => { - const [totalPrice, setTotalPrice] = useState(marketPrice); + const [totalPrice, setTotalPrice] = useState(market.price); + const [priceImpact, setPriceImpact] = useState(0); + const [alertColor, setAlertColor] = useState("neon.500"); const [quantity, setQuantity] = useState(1); const [max, setMax] = useState(0); useEffect(() => { if (type === TradeDirection.Buy) { - setMax(Math.floor(player.cash / marketPrice)); + setMax(calculateMaxQuantity(market.marketPool, player.cash)); } else if (type === TradeDirection.Sell) { const playerQuantity = player.drugs.find( (d) => d.name === drug.name, )?.quantity; setMax(playerQuantity || 0); } - }, [type, drug, marketQuantity, player, marketPrice]); + }, [type, drug, player, market]); useEffect(() => { - setTotalPrice(quantity * marketPrice); + const slippage = calculateSlippage( + { cash: market.marketPool.cash, quantity: market.marketPool.quantity }, + quantity, + type, + ); + + if (slippage.priceImpact > 0.2) { + // >20% + setAlertColor("red"); + } else if (slippage.priceImpact > 0.05) { + // >5% + setAlertColor("neon.200"); + } else { + setAlertColor("neon.500"); + } + + setPriceImpact(slippage.priceImpact); + setTotalPrice(quantity * slippage.newPrice); onChange(quantity); - }, [quantity, marketPrice, onChange]); + }, [quantity, market, onChange]); const onDown = useCallback(() => { if (quantity > 1) { @@ -298,9 +309,15 @@ const QuantitySelector = ({ pointerEvents={max === 0 ? "none" : "all"} > - - {quantity} for {formatCash(totalPrice)} - + + + ({quantity}) for {formatCash(totalPrice)} + + + {(priceImpact * 100).toFixed(2)}% slippage + (estimate) + + @@ -312,7 +329,7 @@ const QuantitySelector = ({ - + @@ -347,7 +363,6 @@ const QuantitySelector = ({ _hover={{ color: "neon.300", }} - p={2} > diff --git a/web/src/utils/defi.ts b/web/src/utils/defi.ts new file mode 100644 index 00000000..0be39454 --- /dev/null +++ b/web/src/utils/defi.ts @@ -0,0 +1,30 @@ +import { TradeDirection } from "@/hooks/state"; +import { SCALING_FACTOR } from "@/hooks/dojo"; +import { Market } from "@/generated/graphql"; + +export const calculateSlippage = ( + market: Market, + tradeAmount: number, + tradeDirection: TradeDirection, +) => { + const k = market.cash * market.quantity; + const currentPrice = market.cash / market.quantity; + + const newQuantity = + tradeDirection === TradeDirection.Buy + ? market.quantity - tradeAmount + : market.quantity + tradeAmount; + const newCash = k / newQuantity; + const newPrice = newCash / newQuantity; + + const priceImpact = Math.abs((newPrice - currentPrice) / currentPrice); + return { priceImpact, newPrice: newPrice / SCALING_FACTOR }; +}; + +export const calculateMaxQuantity = (market: Market, maxCash: number) => { + const k = market.cash * market.quantity; + const maxQuantity = + market.quantity - k / (Number(market.cash) + maxCash * SCALING_FACTOR); + + return Math.floor(maxQuantity); +};