From 9bebec76fed991eb1845605779800464ceaf47fa Mon Sep 17 00:00:00 2001 From: k0beleenders Date: Thu, 5 Dec 2024 18:25:36 +0100 Subject: [PATCH 01/27] feat: store integration yield page & mrgnclient helper functions (wip) --- .../src/components/common/Yield/YieldRow.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/Yield/YieldRow.tsx b/apps/marginfi-v2-trading/src/components/common/Yield/YieldRow.tsx index 415a904da3..bd0789b8bf 100644 --- a/apps/marginfi-v2-trading/src/components/common/Yield/YieldRow.tsx +++ b/apps/marginfi-v2-trading/src/components/common/Yield/YieldRow.tsx @@ -123,26 +123,28 @@ const YieldItem = ({
{bank.meta.tokenSymbol} - {bank.meta.tokenSymbol} + {pool.tokenBank.meta.tokenSymbol}
- {numeralFormatter(bank.info.state.totalDeposits)} + {numeralFormatter(pool.tokenBank.info.state.totalDeposits)} - {usdFormatter.format(bank.info.state.totalDeposits * bank.info.oraclePrice.priceRealtime.price.toNumber())} + {usdFormatter.format( + pool.tokenBank.info.state.totalDeposits * pool.tokenBank.info.oraclePrice.priceRealtime.price.toNumber() + )}
- {percentFormatter.format(aprToApy(bank.info.state.lendingRate))} + {percentFormatter.format(aprToApy(pool.tokenBank.info.state.lendingRate))}
- {percentFormatter.format(aprToApy(bank.info.state.borrowingRate))} + {percentFormatter.format(aprToApy(pool.tokenBank.info.state.borrowingRate))}
@@ -159,8 +161,8 @@ const YieldItem = ({
{isProvidingLiquidity && bank.isActive && ( <> - {numeralFormatter(bank.position.amount)} - {bank.meta.tokenSymbol} + {numeralFormatter(pool.tokenBank.position.amount)} + {pool.tokenBank.meta.tokenSymbol} )}
From 1cca08f53a4d19c63886b24fe6bd071366fb9ab6 Mon Sep 17 00:00:00 2001 From: borcherd Date: Wed, 11 Dec 2024 11:22:00 +0100 Subject: [PATCH 02/27] feat: initial work --- .../action-toggle/action-toggle.tsx | 25 +++ .../components/action-toggle/index.ts | 1 + .../components/amount-input/amount-input.tsx | 53 ++++++ .../amount-input/components/index.ts | 2 + .../components/max-action/index.ts | 1 + .../components/max-action/max-action.tsx | 57 ++++++ .../components/token-select/index.ts | 1 + .../components/token-select/token-select.tsx | 3 + .../components/amount-input/index.ts | 1 + .../trade-box-v2/components/header/header.tsx | 40 ++++ .../trade-box-v2/components/header/index.ts | 1 + .../common/trade-box-v2/components/index.ts | 7 + .../components/info-messages/index.ts | 1 + .../info-messages/info-messages.tsx | 174 +++++++++++++++++ .../components/leverage-slider/index.ts | 1 + .../leverage-slider/leverage-slider.tsx | 31 +++ .../components/settings/components/index.ts | 1 + .../settings/components/slippage.tsx | 151 +++++++++++++++ .../trade-box-v2/components/settings/index.ts | 2 + .../components/settings/settings-dialog.tsx | 61 ++++++ .../components/settings/settings.tsx | 24 +++ .../trade-box-v2/components/stats/index.ts | 1 + .../trade-box-v2/components/stats/stats.tsx | 21 ++ .../components/stats/utils/index.ts | 0 .../components/stats/utils/stats-utils.tsx | 89 +++++++++ .../common/trade-box-v2/contexts/index.ts | 0 .../common/trade-box-v2/hooks/index.ts | 0 .../components/common/trade-box-v2/index.ts | 2 + .../common/trade-box-v2/trade-box.tsx | 180 ++++++++++++++++++ .../common/trade-box-v2/utils/index.ts | 1 + .../trade-box-v2/utils/trade-box.consts.ts | 1 + .../trade-box-v2/utils/trade-box.utils.ts | 0 .../src/pages/trade/[symbol].tsx | 10 +- packages/mrgn-utils/src/rpc.utils.ts | 2 +- 34 files changed, 941 insertions(+), 4 deletions(-) create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/token-select/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/token-select/token-select.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/leverage-slider.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/components/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/components/slippage.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/settings-dialog.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/settings.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/contexts/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.consts.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx new file mode 100644 index 0000000000..2b1d096cf4 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx @@ -0,0 +1,25 @@ +import { TradeSide } from "~/components/common/trade-box-v2/utils"; +import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group"; + +interface ActionToggleProps { + tradeState: TradeSide; + setTradeState: (value: TradeSide) => void; +} + +export const ActionToggle = ({ tradeState, setTradeState }: ActionToggleProps) => { + return ( + value && setTradeState(value as TradeSide)} + > + + Long + + + Short + + + ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/index.ts new file mode 100644 index 0000000000..1f3ddea600 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/index.ts @@ -0,0 +1 @@ +export * from "./action-toggle"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx new file mode 100644 index 0000000000..da580217aa --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; +import { Input } from "~/components/ui/input"; +import { MaxAction } from "./components"; + +interface AmountInputProps { + maxAmount: number; + amount: string; + collateralBank?: ExtendedBankInfo; + + handleAmountChange: (value: string) => void; +} + +export const AmountInput = ({ + amount, + collateralBank, + maxAmount, + + handleAmountChange, +}: AmountInputProps) => { + const amountInputRef = React.useRef(null); + + return ( +
+
+ + {collateralBank?.meta.tokenSymbol.toUpperCase()} + {/* TODO: replace this with pool select */} + +
+ handleAmountChange(e.target.value)} + placeholder="0" + className="bg-transparent shadow-none min-w-[130px] h-auto py-0 pr-0 text-right outline-none focus-visible:outline-none focus-visible:ring-0 border-none text-base font-medium" + /> + {/* {amount !== null && amount > 0 && selectedBank && ( + + {tokenPriceFormatter(amount * selectedBank.info.oraclePrice.priceRealtime.price.toNumber())} + + )} */}{" "} + {/* // TODO: add this usd price */} +
+
+ +
+ ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/index.ts new file mode 100644 index 0000000000..3e00254660 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/index.ts @@ -0,0 +1,2 @@ +export * from "./token-select"; +export * from "./max-action"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/index.ts new file mode 100644 index 0000000000..a0db7a9b66 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/index.ts @@ -0,0 +1 @@ +export * from "./max-action"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx new file mode 100644 index 0000000000..a9107cda05 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx @@ -0,0 +1,57 @@ +import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; +import { clampedNumeralFormatter } from "@mrgnlabs/mrgn-common"; +import React from "react"; + +interface TradeActionProps { + maxAmount: number; + collateralBank: ExtendedBankInfo | undefined; + + setAmount: (amount: string) => void; +} + +export const MaxAction = ({ maxAmount, collateralBank, setAmount }: TradeActionProps) => { + const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); // TODO: remove this + + const maxLabel = React.useMemo((): { + amount: string; + showWalletIcon?: boolean; + label?: string; + } => { + if (!collateralBank) { + return { + amount: "-", + showWalletIcon: false, + }; + } // TODO: is this necessary since collateralBank is defined? + + const formatAmount = (maxAmount?: number, symbol?: string) => + maxAmount !== undefined ? `${clampedNumeralFormatter(maxAmount)} ${symbol?.toUpperCase()}` : "-"; // TODO: use dynamicNumeralFormatter + + return { + amount: formatAmount(maxAmount, collateralBank.meta.tokenSymbol), + label: "Wallet: ", + }; + }, [collateralBank, maxAmount]); + return ( + <> + {collateralBank && ( +
    +
  • + {maxLabel.label} +
    +
    {maxLabel.amount}
    + + +
    +
  • +
+ )} + + ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/token-select/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/token-select/index.ts new file mode 100644 index 0000000000..c540cf96d2 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/token-select/index.ts @@ -0,0 +1 @@ +export * from "./token-select"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/token-select/token-select.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/token-select/token-select.tsx new file mode 100644 index 0000000000..e626aad3b9 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/token-select/token-select.tsx @@ -0,0 +1,3 @@ +export const TokenSelect = () => { + return
TokenSelect
; +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/index.ts new file mode 100644 index 0000000000..9103523b43 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/index.ts @@ -0,0 +1 @@ +export * from "./amount-input"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx new file mode 100644 index 0000000000..3f9d95c3bf --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx @@ -0,0 +1,40 @@ +import { IconChevronDown } from "@tabler/icons-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import * as React from "react"; +import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; + +interface HeaderProps { + activePool: ArenaPoolV2Extended; +} + +export const Header = ({ activePool }: HeaderProps) => { + // TODO: change styling of this, this isnt the best + + return ( +
+ + {activePool.tokenBank.meta.tokenSymbol} + {activePool.tokenBank.meta.tokenSymbol} + + +
+
+ Entry price + $122.00 +
+
+ 24h volume + $1.65m +
+
+
+ ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/index.ts new file mode 100644 index 0000000000..49ac70fe21 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/index.ts @@ -0,0 +1 @@ +export * from "./header"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts new file mode 100644 index 0000000000..178c0d4604 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts @@ -0,0 +1,7 @@ +export * from "./action-toggle"; +export * from "./leverage-slider"; +export * from "./amount-input"; +export * from "./header"; +export * from "./info-messages"; +export * from "./stats"; +export * from "./settings"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/index.ts new file mode 100644 index 0000000000..1e2ac5c4bc --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/index.ts @@ -0,0 +1 @@ +export * from "./info-messages"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx new file mode 100644 index 0000000000..4dabc2a1cd --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx @@ -0,0 +1,174 @@ +import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; +import { Wallet } from "@mrgnlabs/mrgn-common"; +import { ActionMessageType } from "@mrgnlabs/mrgn-utils"; +import { Connection } from "@solana/web3.js"; +import { IconAlertTriangle, IconExternalLink } from "@tabler/icons-react"; +import Link from "next/link"; +import { ActionBox } from "~/components"; +import { Button } from "~/components/ui/button"; +import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; + +interface InfoMessagesProps { + connected: boolean; + tradeState: string; + activePool: ArenaPoolV2Extended; + isActiveWithCollat: boolean; + actionMethods: ActionMessageType[]; + additionalChecks?: ActionMessageType; + setIsWalletOpen: (value: boolean) => void; + fetchTradeState: ({ + connection, + wallet, + refresh, + }: { + connection?: Connection; + wallet?: Wallet; + refresh?: boolean; + }) => Promise; + connection: any; + wallet: any; +} + +export const InfoMessages = ({ + connected, + tradeState, + activePool, + isActiveWithCollat, + actionMethods = [], + additionalChecks, + setIsWalletOpen, + fetchTradeState, + connection, + wallet, +}: InfoMessagesProps) => { + const renderLongWarning = () => ( +
+ +
+

+ You need to hold {activePool?.tokenBank.meta.tokenSymbol} to open a long position.{" "} + +

+
+
+ ); + + const renderShortWarning = () => ( +
+ +
+

+ You need to hold {activePool?.quoteBank.meta.tokenSymbol} to open a short position.{" "} + +

+
+
+ ); + + const renderActionMethodMessages = () => + actionMethods.concat(additionalChecks ?? []).map( + (actionMethod, idx) => + actionMethod.description && ( +
+
+ +
+

{actionMethod.description}

+ {actionMethod.action && ( + console.log("Position added"), + onComplete: () => fetchTradeState({ connection, wallet }), + }} + dialogProps={{ + trigger: ( + + ), + title: `${actionMethod.action.type} ${actionMethod.action.bank.meta.tokenSymbol}`, + }} + /> + )} + {actionMethod.link && ( +

+ + {" "} + {actionMethod.linkText || "Read more"} + +

+ )} +
+
+
+ ) + ); + + const renderDepositCollateralDialog = () => ( + console.log("Deposit Collateral"), + onComplete: () => fetchTradeState({ connection, wallet }), + }} + dialogProps={{ + trigger: , + title: `Supply ${activePool.quoteBank.meta.tokenSymbol}`, + }} + /> + ); + + const renderContent = () => { + if (!connected) return null; + + switch (true) { + case tradeState === "long" && activePool?.tokenBank.userInfo.tokenAccount.balance === 0: + return renderLongWarning(); + + case tradeState === "short" && activePool?.quoteBank.userInfo.tokenAccount.balance === 0: + return renderShortWarning(); + + case isActiveWithCollat: + return renderActionMethodMessages(); + + default: + return renderDepositCollateralDialog(); + } + }; + + return
{renderContent()}
; +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/index.ts new file mode 100644 index 0000000000..476b5d1373 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/index.ts @@ -0,0 +1 @@ +export * from "./leverage-slider"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/leverage-slider.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/leverage-slider.tsx new file mode 100644 index 0000000000..debc280a8b --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/leverage-slider.tsx @@ -0,0 +1,31 @@ +import { Label } from "~/components/ui/label"; +import { Slider } from "~/components/ui/slider"; + +interface LeverageSliderProps { + leverage: number; + maxLeverage: number; + setLeverage: (value: number) => void; +} + +export const LeverageSlider = ({ leverage, maxLeverage, setLeverage }: LeverageSliderProps) => { + return ( +
+
+ + {leverage.toFixed(2)}x +
+ { + if (value[0] > maxLeverage) return; + setLeverage(value[0]); + }} + /> +
+ ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/components/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/components/index.ts new file mode 100644 index 0000000000..50ea3be54a --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/components/index.ts @@ -0,0 +1 @@ +export * from "./slippage"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/components/slippage.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/components/slippage.tsx new file mode 100644 index 0000000000..e428499f33 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/components/slippage.tsx @@ -0,0 +1,151 @@ +import React from "react"; + +import { IconInfoCircle } from "@tabler/icons-react"; +import { useForm } from "react-hook-form"; +import { cn } from "@mrgnlabs/mrgn-utils"; + +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; +import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group"; +import { Label } from "~/components/ui/label"; +import { Form, FormControl, FormField, FormItem, FormMessage } from "~/components/ui/form"; + +type SlippageProps = { + slippageBps: number; + setSlippageBps: (value: number) => void; + toggleSettings: (mode: boolean) => void; +}; + +const DEFAULT_SLIPPAGE_BPS = 100; + +const slippageOptions = [ + { + label: "Low", + value: 0.3, + }, + { + label: "Normal", + value: 0.5, + }, + { + label: "High", + value: 1, + }, +]; + +interface SlippageForm { + slippageBps: number; +} + +export const Slippage = ({ slippageBps, setSlippageBps, toggleSettings }: SlippageProps) => { + const form = useForm({ + defaultValues: { + slippageBps: slippageBps, + }, + }); + const formWatch = form.watch(); + + const isCustomSlippage = React.useMemo( + () => (slippageOptions.find((value) => value.value === formWatch.slippageBps) ? false : true), + [formWatch.slippageBps] + ); + + function onSubmit(data: SlippageForm) { + setSlippageBps(data.slippageBps); + toggleSettings(false); + } + + return ( +
+
+ +

+ Set transaction slippage{" "} + + + + + + +
+

Priority fees are paid to the Solana network.

+

This additional fee helps boost how a transaction is prioritized.

+
+
+
+
+

+ ( + + + { + field.onChange(Number(value)); + }} + defaultValue={field.value.toString()} + className="flex gap-4 justify-between" + > + {slippageOptions.map((option) => ( +
+ + +
+ ))} +
+
+ +
+ )} + /> +

or set manually

+ + ( + + +
+ field.onChange(e)} + className={cn("h-auto py-3 px-4 border", isCustomSlippage && "bg-accent")} + /> + % +
+
+ +
+ )} + /> + + + + +
+ ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/index.ts new file mode 100644 index 0000000000..7b2a8f6d92 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/index.ts @@ -0,0 +1,2 @@ +export * from "./settings"; +export * from "./settings-dialog"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/settings-dialog.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/settings-dialog.tsx new file mode 100644 index 0000000000..1bd39b34f1 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/settings-dialog.tsx @@ -0,0 +1,61 @@ +import React from "react"; + +import { Desktop, Mobile } from "@mrgnlabs/mrgn-utils"; + +import { useIsMobile } from "~/hooks/use-is-mobile"; +import { Dialog, DialogTrigger, DialogContent } from "~/components/ui/dialog"; + +import { TradingBoxSettings } from "./settings"; + +type SettingsDialogProps = { + slippageBps: number; + setSlippageBps: (value: number) => void; + + children: React.ReactNode; + isDialogTriggered?: boolean; +}; + +export const TradingBoxSettingsDialog = ({ + slippageBps, + setSlippageBps, + children, + isDialogTriggered = false, +}: SettingsDialogProps) => { + const [isDialogOpen, setIsDialogOpen] = React.useState(false); + const isMobile = useIsMobile(); + + React.useEffect(() => { + setIsDialogOpen(isDialogTriggered); + }, [setIsDialogOpen, isDialogTriggered]); + + return ( + setIsDialogOpen(open)}> + + {isDialogOpen &&
} + {children} + + setIsDialogOpen(mode)} + slippageBps={slippageBps} + setSlippageBps={setSlippageBps} + /> + + + + {children} + +
+ setIsDialogOpen(mode)} + slippageBps={slippageBps} + setSlippageBps={setSlippageBps} + /> +
+
+
+
+ ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/settings.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/settings.tsx new file mode 100644 index 0000000000..9ea8dfe781 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/settings/settings.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import { IconArrowLeft } from "@tabler/icons-react"; + +import { Slippage } from "./components"; + +type TradingBoxSettingsProps = { + toggleSettings: (mode: boolean) => void; + slippageBps: number; + setSlippageBps: (value: number) => void; +}; + +export const TradingBoxSettings = ({ toggleSettings, slippageBps, setSlippageBps }: TradingBoxSettingsProps) => { + return ( +
+ +
+ +
+
+ ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/index.ts new file mode 100644 index 0000000000..211e8756df --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/index.ts @@ -0,0 +1 @@ +export * from "./stats"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx new file mode 100644 index 0000000000..a2fb2e9dfc --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; + +interface StatsProps { + activePool: ArenaPoolV2Extended; +} +export const Stats = ({ activePool }: StatsProps) => { + React.useEffect(() => { + if (activePool) { + // generateStats( + // activeGroup.accountSummary, + // activeGroup.pool.token, + // activeGroup.pool.quoteTokens[0], + // null, + // null, + // false + // ); + } + }, [activePool]); + return
Stats
; +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx new file mode 100644 index 0000000000..2726e3b6dc --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx @@ -0,0 +1,89 @@ +import { MarginRequirementType, SimulationResult } from "@mrgnlabs/marginfi-client-v2"; +import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; +import { LoopActionTxns } from "@mrgnlabs/mrgn-utils"; + +import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; + +interface generateTradeStatsProps { + accountSummary: AccountSummary; + extendedPool: ArenaPoolV2Extended; + simulationResult: SimulationResult | null; + actionTxns: LoopActionTxns; +} + +export function generateTradeStats(props: generateTradeStatsProps) { + let simStats = null; + + if (props.simulationResult) { + simStats = getSimulationStats(props.simulationResult, props.extendedPool); + } + + const stats = []; +} + +export function getSimulationStats(simulationResult: SimulationResult, extendedPool: ArenaPoolV2Extended) { + const { assets, liabilities } = simulationResult.marginfiAccount.computeHealthComponents( + MarginRequirementType.Maintenance + ); + const { assets: assetsInit } = simulationResult.marginfiAccount.computeHealthComponents( + MarginRequirementType.Initial + ); + + const healthFactor = assets.minus(liabilities).dividedBy(assets).toNumber(); + const liquidationPrice = simulationResult.marginfiAccount.computeLiquidationPriceForBank( + extendedPool.tokenBank.address + ); + // const { lendingRate, borrowingRate } = simulationResult.banks.get(bank.address.toBase58())!.computeInterestRates(); + + // Token position + const tokenPosition = simulationResult.marginfiAccount.activeBalances.find( + (b) => b.active && b.bankPk.equals(extendedPool.tokenBank.address) + ); + let tokenPositionAmount = 0; + if (tokenPosition && tokenPosition.liabilityShares.gt(0)) { + tokenPositionAmount = tokenPosition.computeQuantityUi(extendedPool.tokenBank.info.rawBank).liabilities.toNumber(); + } else if (tokenPosition && tokenPosition.assetShares.gt(0)) { + tokenPositionAmount = tokenPosition.computeQuantityUi(extendedPool.tokenBank.info.rawBank).assets.toNumber(); + } + + // usdc position + const usdcPosition = simulationResult.marginfiAccount.activeBalances.find( + (b) => b.active && b.bankPk.equals(extendedPool.quoteBank.address) + ); + let usdcPositionAmount = 0; + if (usdcPosition && usdcPosition.liabilityShares.gt(0)) { + usdcPositionAmount = usdcPosition.computeQuantityUi(extendedPool.quoteBank.info.rawBank).liabilities.toNumber(); + } else if (usdcPosition && usdcPosition.assetShares.gt(0)) { + usdcPositionAmount = usdcPosition.computeQuantityUi(extendedPool.quoteBank.info.rawBank).assets.toNumber(); + } + + const availableCollateral = simulationResult.marginfiAccount.computeFreeCollateral().toNumber(); + + return { + tokenPositionAmount, + usdcPositionAmount, + healthFactor, + liquidationPrice, + }; +} // TODO: check if this is correct, copy pasted old code + +export function getCurrentStats(accountSummary: AccountSummary, extendedPool: ArenaPoolV2Extended) { + const tokenPositionAmount = extendedPool.tokenBank?.isActive ? extendedPool.tokenBank.position.amount : 0; + const usdcPositionAmount = extendedPool.quoteBank?.isActive ? extendedPool.quoteBank.position.amount : 0; + const healthFactor = !accountSummary.balance || !accountSummary.healthFactor ? 1 : accountSummary.healthFactor; + + // always token asset liq price + const liquidationPrice = + extendedPool.tokenBank.isActive && + extendedPool.tokenBank.position.liquidationPrice && + extendedPool.tokenBank.position.liquidationPrice > 0.01 + ? extendedPool.tokenBank.position.liquidationPrice + : null; + + return { + tokenPositionAmount, + usdcPositionAmount, + healthFactor, + liquidationPrice, + }; +} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/contexts/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/contexts/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/index.ts new file mode 100644 index 0000000000..8ab72d9631 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/index.ts @@ -0,0 +1,2 @@ +export * from "./trade-box"; +export * from "./utils"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx new file mode 100644 index 0000000000..4b96411564 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -0,0 +1,180 @@ +"use client"; + +import React from "react"; +import { computeMaxLeverage } from "@mrgnlabs/marginfi-client-v2"; +import { ActionMessageType, cn, formatAmount, LoopActionTxns, useConnection } from "@mrgnlabs/mrgn-utils"; + +// import { GroupData } from "~/store/tradeStore"; +import { ArenaPoolV2 } from "~/store/tradeStoreV2"; +import { TradeSide } from "~/components/common/trade-box-v2/utils"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader } from "~/components/ui/card"; + +import { ActionToggle, AmountInput, Header, LeverageSlider, Stats, TradingBoxSettingsDialog } from "./components"; +import { useTradeStoreV2, useUiStore } from "~/store"; +import { IconSettings } from "@tabler/icons-react"; +import { InfoMessages } from "./components/info-messages/info-messages"; +import { useWallet, useWalletStore } from "~/components/wallet-v2"; +import { checkLoopingActionAvailable } from "../TradingBox/tradingBox.utils"; +import { useExtendedPool } from "~/hooks/useExtendedPools"; +import { useMarginfiClient } from "~/hooks/useMarginfiClient"; +import { useWrappedAccount } from "~/hooks/useWrappedAccount"; + +interface TradeBoxV2Props { + activePool: ArenaPoolV2; + side?: TradeSide; +} + +export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { + const activePoolExtended = useExtendedPool(activePool); + const client = useMarginfiClient({ groupPk: activePoolExtended.groupPk }); + const { accountSummary, wrappedAccount } = useWrappedAccount({ + client, + groupPk: activePoolExtended.groupPk, + banks: [activePoolExtended.tokenBank, activePoolExtended.quoteBank], + }); + const { walletContextState, wallet, connected } = useWallet(); + const [slippageBps, setSlippageBps] = useUiStore((state) => [state.slippageBps, state.setSlippageBps]); + const [setIsWalletOpen] = useWalletStore((state) => [state.setIsWalletOpen]); + const [fetchTradeState, nativeSolBalance, setIsRefreshingStore, refreshGroup] = useTradeStoreV2((state) => [ + state.fetchTradeState, + state.nativeSolBalance, + state.setIsRefreshingStore, + state.refreshGroup, + ]); + const { connection } = useConnection(); + + const [tradeState, setTradeState] = React.useState(side); + const [leverage, setLeverage] = React.useState(0); + const [amount, setAmount] = React.useState(""); // use store for this maybe + const [tradeActionTxns, setTradeActionTxns] = React.useState(null); + const [additionalChecks, setAdditionalChecks] = React.useState(); + + const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); // The fuck is this lol? + + const collateralBank = React.useMemo(() => { + if (activePoolExtended) { + if (tradeState === "short") { + return activePoolExtended.quoteBank; + } else { + return activePoolExtended.tokenBank; + } + } + }, [activePoolExtended, tradeState]); + + const maxLeverage = React.useMemo(() => { + if (activePoolExtended) { + const deposit = + tradeState === "long" ? activePoolExtended.tokenBank.info.rawBank : activePoolExtended.quoteBank.info.rawBank; + const borrow = + tradeState === "long" ? activePoolExtended.quoteBank.info.rawBank : activePoolExtended.tokenBank.info.rawBank; + + const { maxLeverage } = computeMaxLeverage(deposit, borrow); + return maxLeverage; + } + return 0; + }, [activePoolExtended, tradeState]); + + const isActiveWithCollat = true; // the fuuuuck? + + const maxAmount = React.useMemo(() => { + if (collateralBank) { + return collateralBank.userInfo.maxDeposit; + } + return 0; + }, [collateralBank]); // Remove this and just use the collateralBank.userInfo.maxDeposit + + const actionMethods = React.useMemo( + () => + checkLoopingActionAvailable({ + amount, + connected, + activePoolExtended, + loopActionTxns: tradeActionTxns, + tradeSide: tradeState, + }), // TODO: do we need a more tailored check for trading instead of looping? + [amount, connected, activePoolExtended, tradeActionTxns, tradeState] + ); + + const handleAmountChange = React.useCallback( + (amountRaw: string) => { + const amount = formatAmount(amountRaw, maxAmount, collateralBank ?? null, numberFormater); + setAmount(amount); + }, + [maxAmount, collateralBank, numberFormater] + ); + + const [Stats, setStats] = React.useState(<>); + + // React.useEffect(() => { + // if (activePool) { + // setStats( + // generateStats( + // activePool.accountSummary, + // activePool.pool.token, + // activePool.pool.quoteTokens[0], + // null, + // null, + // false + // ) + // ); + // } + // }, [activePool]); + + return ( + + +
+ + +
+ + + +
+ {tradeState === "long" ? "Long" : "Short"} amount + 100 {collateralBank?.meta.tokenSymbol} +
+ + + setSlippageBps(value * 100)} + slippageBps={slippageBps / 100} + > +
+ +
+
+
+ {/* */} +
+ + ); +}; + +/* +TODO: +- when wallet is connected but store is loading, show to user + +*/ diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts new file mode 100644 index 0000000000..f7cafa793c --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts @@ -0,0 +1 @@ +export * from "./trade-box.consts"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.consts.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.consts.ts new file mode 100644 index 0000000000..5b479ae6c0 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.consts.ts @@ -0,0 +1 @@ +export type TradeSide = "long" | "short"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/marginfi-v2-trading/src/pages/trade/[symbol].tsx b/apps/marginfi-v2-trading/src/pages/trade/[symbol].tsx index cc5fd0b297..1e3dec560e 100644 --- a/apps/marginfi-v2-trading/src/pages/trade/[symbol].tsx +++ b/apps/marginfi-v2-trading/src/pages/trade/[symbol].tsx @@ -16,6 +16,7 @@ import { Loader } from "~/components/common/Loader"; import { ArenaPoolV2 } from "~/store/tradeStoreV2"; import { GetStaticPaths, GetStaticProps } from "next"; import { StaticArenaProps, getArenaStaticProps } from "~/utils"; +import { TradeBoxV2 } from "~/components/common/trade-box-v2"; export const getStaticPaths: GetStaticPaths = async () => { return { @@ -83,12 +84,15 @@ export default function TradeSymbolPage({ initialData }: StaticArenaProps) {
-
- -
+ {/*
+ +
*/}
+
+ +
{!isMobile && ( diff --git a/packages/mrgn-utils/src/rpc.utils.ts b/packages/mrgn-utils/src/rpc.utils.ts index 34987669c1..6920f1bc82 100644 --- a/packages/mrgn-utils/src/rpc.utils.ts +++ b/packages/mrgn-utils/src/rpc.utils.ts @@ -1,5 +1,5 @@ export function generateEndpoint(endpoint: string, rpcProxyKey: string = "") { if (!rpcProxyKey) return endpoint; const hash = Buffer.from(rpcProxyKey, "utf8").toString("base64").replace(/[/+=]/g, ""); - return `${endpoint}/${hash}`; + return `${endpoint}`; } From c5eb15d481121c0f8b44cd9fe1ee8c9978ecf89c Mon Sep 17 00:00:00 2001 From: borcherd Date: Wed, 11 Dec 2024 12:21:58 +0100 Subject: [PATCH 03/27] feat: refactor stats --- .../trade-box-v2/components/stats/stats.tsx | 46 ++++-- .../components/stats/utils/stats-utils.tsx | 156 +++++++++++++----- .../common/trade-box-v2/trade-box.tsx | 27 +-- 3 files changed, 154 insertions(+), 75 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx index a2fb2e9dfc..9d52267df1 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx @@ -1,21 +1,39 @@ import React from "react"; import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; +import { generateTradeStats } from "./utils/stats-utils"; +import { cn, LoopActionTxns } from "@mrgnlabs/mrgn-utils"; +import { SimulationResult } from "@mrgnlabs/marginfi-client-v2"; +import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; +import { ActionStatItem } from "~/components/action-box-v2/components/action-stats/action-stat-item"; interface StatsProps { activePool: ArenaPoolV2Extended; + accountSummary: AccountSummary | null; + simulationResult: SimulationResult | null; + actionTxns: LoopActionTxns | null; } -export const Stats = ({ activePool }: StatsProps) => { - React.useEffect(() => { - if (activePool) { - // generateStats( - // activeGroup.accountSummary, - // activeGroup.pool.token, - // activeGroup.pool.quoteTokens[0], - // null, - // null, - // false - // ); - } - }, [activePool]); - return
Stats
; +export const Stats = ({ activePool, accountSummary, simulationResult, actionTxns }: StatsProps) => { + const stats = React.useMemo( + () => + generateTradeStats({ + accountSummary: accountSummary, + extendedPool: activePool, + simulationResult: simulationResult, + actionTxns: actionTxns, + }), + [activePool, accountSummary, simulationResult, actionTxns] + ); + return ( + <> + {stats && ( +
+ {stats.map((stat, idx) => ( + + + + ))} +
+ )} + + ); }; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx index 2726e3b6dc..b951054acf 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx @@ -1,39 +1,136 @@ import { MarginRequirementType, SimulationResult } from "@mrgnlabs/marginfi-client-v2"; import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; +import { percentFormatter, tokenPriceFormatter, usdFormatter } from "@mrgnlabs/mrgn-common"; import { LoopActionTxns } from "@mrgnlabs/mrgn-utils"; +import { IconArrowRight } from "@tabler/icons-react"; +import { PreviewStat } from "~/components/action-box-v2/utils"; +import { IconPyth } from "~/components/ui/icons"; +import { IconSwitchboard } from "~/components/ui/icons"; import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; interface generateTradeStatsProps { - accountSummary: AccountSummary; + accountSummary: AccountSummary | null; extendedPool: ArenaPoolV2Extended; simulationResult: SimulationResult | null; - actionTxns: LoopActionTxns; + actionTxns: LoopActionTxns | null; } export function generateTradeStats(props: generateTradeStatsProps) { - let simStats = null; + const stats: PreviewStat[] = []; + // entry price stat + stats.push({ + label: "Entry price", + value: () => <>{tokenPriceFormatter(props.extendedPool.tokenBank.info.state.price)}, + }); + + // simulation stat if (props.simulationResult) { - simStats = getSimulationStats(props.simulationResult, props.extendedPool); + const simStats = getSimulationStats(props.simulationResult, props.extendedPool); + const currentLiquidationPrice = + props.extendedPool.tokenBank.isActive && + props.extendedPool.tokenBank.position.liquidationPrice && + props.extendedPool.tokenBank.position.liquidationPrice > 0.01 + ? usdFormatter.format(props.extendedPool.tokenBank.position.liquidationPrice) + : null; + const simulatedLiqPrice = simStats?.liquidationPrice ? usdFormatter.format(simStats?.liquidationPrice) : null; + const showLiqComparison = currentLiquidationPrice && simulatedLiqPrice; + stats.push({ + label: "Liquidation price", + value: () => ( + <> + {currentLiquidationPrice && {currentLiquidationPrice}} + {showLiqComparison && } + {simulatedLiqPrice && {simulatedLiqPrice}} + + ), + }); + } + + // platform fee stat + const platformFeeBps = props.actionTxns?.actionQuote?.platformFee + ? Number(props.actionTxns.actionQuote.platformFee?.feeBps) + : undefined; + if (platformFeeBps) { + stats.push({ + label: "Platform fee", + value: () => <>{percentFormatter.format(platformFeeBps / 10000)}, + }); } - const stats = []; + // price impact stat + const priceImpactPct = props.actionTxns?.actionQuote + ? Number(props.actionTxns.actionQuote.priceImpactPct) + : undefined; + if (priceImpactPct) { + stats.push({ + label: "Price impact", + color: priceImpactPct > 0.05 ? "DESTRUCTIVE" : priceImpactPct > 0.01 ? "ALERT" : "SUCCESS", + value: () => <>{percentFormatter.format(priceImpactPct)}, + }); + } + + // oracle stat + let oracle = ""; + switch (props.extendedPool.tokenBank.info.rawBank.config.oracleSetup) { + case "PythLegacy": + oracle = "Pyth"; + break; + case "PythPushOracle": + oracle = "Pyth"; + break; + case "SwitchboardV2": + oracle = "Switchboard"; + break; + } + stats.push({ + label: "Oracle", + value: () => ( + <> + {oracle} + {oracle === "Pyth" ? : } + + ), + }); + + const accountSummary = props.accountSummary; + if (accountSummary) { + // total deposits stat + stats.push({ + label: "Total deposits", + value: () => ( + <> + {props.extendedPool.tokenBank.info.state.totalDeposits.toFixed(2)}{" "} + {props.extendedPool.tokenBank.meta.tokenSymbol} + + ), + }); + + // total borrows stat + stats.push({ + label: "Total borrows", + value: () => ( + <> + {props.extendedPool.tokenBank.info.state.totalBorrows.toFixed(2)}{" "} + {props.extendedPool.tokenBank.meta.tokenSymbol} + + ), + }); + } + + return stats; } export function getSimulationStats(simulationResult: SimulationResult, extendedPool: ArenaPoolV2Extended) { const { assets, liabilities } = simulationResult.marginfiAccount.computeHealthComponents( MarginRequirementType.Maintenance ); - const { assets: assetsInit } = simulationResult.marginfiAccount.computeHealthComponents( - MarginRequirementType.Initial - ); const healthFactor = assets.minus(liabilities).dividedBy(assets).toNumber(); const liquidationPrice = simulationResult.marginfiAccount.computeLiquidationPriceForBank( extendedPool.tokenBank.address ); - // const { lendingRate, borrowingRate } = simulationResult.banks.get(bank.address.toBase58())!.computeInterestRates(); // Token position const tokenPosition = simulationResult.marginfiAccount.activeBalances.find( @@ -46,44 +143,21 @@ export function getSimulationStats(simulationResult: SimulationResult, extendedP tokenPositionAmount = tokenPosition.computeQuantityUi(extendedPool.tokenBank.info.rawBank).assets.toNumber(); } - // usdc position - const usdcPosition = simulationResult.marginfiAccount.activeBalances.find( + // quote position + const quotePosition = simulationResult.marginfiAccount.activeBalances.find( (b) => b.active && b.bankPk.equals(extendedPool.quoteBank.address) ); - let usdcPositionAmount = 0; - if (usdcPosition && usdcPosition.liabilityShares.gt(0)) { - usdcPositionAmount = usdcPosition.computeQuantityUi(extendedPool.quoteBank.info.rawBank).liabilities.toNumber(); - } else if (usdcPosition && usdcPosition.assetShares.gt(0)) { - usdcPositionAmount = usdcPosition.computeQuantityUi(extendedPool.quoteBank.info.rawBank).assets.toNumber(); + let quotePositionAmount = 0; + if (quotePosition && quotePosition.liabilityShares.gt(0)) { + quotePositionAmount = quotePosition.computeQuantityUi(extendedPool.quoteBank.info.rawBank).liabilities.toNumber(); + } else if (quotePosition && quotePosition.assetShares.gt(0)) { + quotePositionAmount = quotePosition.computeQuantityUi(extendedPool.quoteBank.info.rawBank).assets.toNumber(); } - const availableCollateral = simulationResult.marginfiAccount.computeFreeCollateral().toNumber(); - - return { - tokenPositionAmount, - usdcPositionAmount, - healthFactor, - liquidationPrice, - }; -} // TODO: check if this is correct, copy pasted old code - -export function getCurrentStats(accountSummary: AccountSummary, extendedPool: ArenaPoolV2Extended) { - const tokenPositionAmount = extendedPool.tokenBank?.isActive ? extendedPool.tokenBank.position.amount : 0; - const usdcPositionAmount = extendedPool.quoteBank?.isActive ? extendedPool.quoteBank.position.amount : 0; - const healthFactor = !accountSummary.balance || !accountSummary.healthFactor ? 1 : accountSummary.healthFactor; - - // always token asset liq price - const liquidationPrice = - extendedPool.tokenBank.isActive && - extendedPool.tokenBank.position.liquidationPrice && - extendedPool.tokenBank.position.liquidationPrice > 0.01 - ? extendedPool.tokenBank.position.liquidationPrice - : null; - return { tokenPositionAmount, - usdcPositionAmount, + quotePositionAmount, healthFactor, liquidationPrice, }; -} +} // TODO: a lot of this code is copy pasted from old code, need to clean up diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 4b96411564..a34d222291 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -104,23 +104,6 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { [maxAmount, collateralBank, numberFormater] ); - const [Stats, setStats] = React.useState(<>); - - // React.useEffect(() => { - // if (activePool) { - // setStats( - // generateStats( - // activePool.accountSummary, - // activePool.pool.token, - // activePool.pool.quoteTokens[0], - // null, - // null, - // false - // ) - // ); - // } - // }, [activePool]); - return ( @@ -145,8 +128,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { tradeState={tradeState} activePool={activePoolExtended} isActiveWithCollat={isActiveWithCollat} - // actionMethods={actionMethods} - actionMethods={[]} + actionMethods={actionMethods} additionalChecks={additionalChecks} setIsWalletOpen={setIsWalletOpen} fetchTradeState={fetchTradeState} @@ -167,7 +149,12 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => {
- {/* */} + ); From f8efa69b5a9c08a28c8d3c118af4f7f952108363 Mon Sep 17 00:00:00 2001 From: borcherd Date: Wed, 11 Dec 2024 13:53:47 +0100 Subject: [PATCH 04/27] feat: ui improvements --- .../trade-box-v2/components/header/header.tsx | 35 ++++++---- .../info-messages/info-messages.tsx | 5 ++ .../common/trade-box-v2/trade-box.tsx | 67 +++++++++++++------ 3 files changed, 75 insertions(+), 32 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx index 3f9d95c3bf..20518d21fa 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; import * as React from "react"; +import { TokenCombobox } from "~/components/common/TokenCombobox"; import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; interface HeaderProps { @@ -10,21 +11,29 @@ interface HeaderProps { } export const Header = ({ activePool }: HeaderProps) => { - // TODO: change styling of this, this isnt the best + const router = useRouter(); return ( -
- - {activePool.tokenBank.meta.tokenSymbol} - {activePool.tokenBank.meta.tokenSymbol} - - +
+ { + router.push(`/trade/${pool.groupPk.toBase58()}`); + }} + > +
+ {activePool.tokenBank.meta.tokenSymbol} +

+ {activePool.tokenBank.meta.tokenName} +

+
+
Entry price diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx index 4dabc2a1cd..b6492805e3 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx @@ -153,19 +153,24 @@ export const InfoMessages = ({ ); const renderContent = () => { + console.log("actionMethods", actionMethods); if (!connected) return null; switch (true) { case tradeState === "long" && activePool?.tokenBank.userInfo.tokenAccount.balance === 0: + console.log("renderLongWarning"); return renderLongWarning(); case tradeState === "short" && activePool?.quoteBank.userInfo.tokenAccount.balance === 0: + console.log("renderShortWarning"); return renderShortWarning(); case isActiveWithCollat: + console.log("renderActionMethodMessages"); return renderActionMethodMessages(); default: + console.log("renderDepositCollateralDialog"); return renderDepositCollateralDialog(); } }; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index a34d222291..2ff4b71d38 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -2,7 +2,7 @@ import React from "react"; import { computeMaxLeverage } from "@mrgnlabs/marginfi-client-v2"; -import { ActionMessageType, cn, formatAmount, LoopActionTxns, useConnection } from "@mrgnlabs/mrgn-utils"; +import { ActionMessageType, cn, formatAmount, LoopActionTxns, useConnection, usePrevious } from "@mrgnlabs/mrgn-utils"; // import { GroupData } from "~/store/tradeStore"; import { ArenaPoolV2 } from "~/store/tradeStoreV2"; @@ -50,8 +50,24 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { const [tradeActionTxns, setTradeActionTxns] = React.useState(null); const [additionalChecks, setAdditionalChecks] = React.useState(); + const prevTradeState = usePrevious(tradeState); + + React.useEffect(() => { + if (tradeState !== prevTradeState) { + clearStates(); + } + }, [tradeState, prevTradeState]); + const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); // The fuck is this lol? + const leveragedAmount = React.useMemo(() => { + if (tradeState === "long") { + return tradeActionTxns?.actualDepositAmount; + } else { + return tradeActionTxns?.borrowAmount.toNumber(); + } + }, [tradeState, tradeActionTxns]); + const collateralBank = React.useMemo(() => { if (activePoolExtended) { if (tradeState === "short") { @@ -82,7 +98,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { return collateralBank.userInfo.maxDeposit; } return 0; - }, [collateralBank]); // Remove this and just use the collateralBank.userInfo.maxDeposit + }, [collateralBank]); // Remove this and just use the collateralBank.userInfo.maxDeposit? const actionMethods = React.useMemo( () => @@ -104,12 +120,19 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { [maxAmount, collateralBank, numberFormater] ); + const clearStates = () => { + setAmount(""); + setTradeActionTxns(null); + setLeverage(1); + setAdditionalChecks(undefined); + }; + return ( - +
- +
{ />
- {tradeState === "long" ? "Long" : "Short"} amount - 100 {collateralBank?.meta.tokenSymbol} + Size of {tradeState} + + {`${ + leveragedAmount ? leveragedAmount.toFixed(activePoolExtended.tokenBank.info.state.mintDecimals) : 0 + } ${collateralBank?.meta.tokenSymbol}`} +
- + {actionMethods && actionMethods.some((method) => method.description) && ( + + )} @@ -142,7 +171,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { setSlippageBps={(value) => setSlippageBps(value * 100)} slippageBps={slippageBps / 100} > -
+
From b20d20c9b7f35b2ca933ca75e52a2d856ed4fcd8 Mon Sep 17 00:00:00 2001 From: borcherd Date: Wed, 11 Dec 2024 17:07:17 +0100 Subject: [PATCH 05/27] feat: tx simulation --- .../action-toggle/action-toggle.tsx | 5 +- .../components/amount-input/amount-input.tsx | 6 +- .../components/max-action/max-action.tsx | 3 +- .../info-messages/info-messages.tsx | 2 +- .../common/trade-box-v2/contexts/index.ts | 0 .../common/trade-box-v2/hooks/index.ts | 2 + .../trade-box-v2/hooks/use-action-amounts.ts | 49 +++++ .../hooks/use-trade-simulation.ts | 196 ++++++++++++++++++ .../common/trade-box-v2/store/index.ts | 5 + .../trade-box-v2/store/trade-box-store.tsx | 184 ++++++++++++++++ .../common/trade-box-v2/trade-box.tsx | 180 ++++++++++------ .../common/trade-box-v2/utils/index.ts | 2 + .../trade-box-v2/utils/trade-box.utils.ts | 104 ++++++++++ .../utils/trade-simulation.utils.ts | 119 +++++++++++ .../src/pages/trade/[symbol].tsx | 10 +- 15 files changed, 794 insertions(+), 73 deletions(-) delete mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/contexts/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-action-amounts.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx index 2b1d096cf4..7e13b6178c 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx @@ -12,7 +12,10 @@ export const ActionToggle = ({ tradeState, setTradeState }: ActionToggleProps) = type="single" className="w-full gap-4" value={tradeState} - onValueChange={(value) => value && setTradeState(value as TradeSide)} + onValueChange={(value) => { + console.log("value", value); + value && setTradeState(value as TradeSide); + }} > Long diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx index da580217aa..162cdad27f 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx @@ -3,11 +3,12 @@ import React from "react"; import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { Input } from "~/components/ui/input"; import { MaxAction } from "./components"; +import { ArenaBank } from "~/store/tradeStoreV2"; interface AmountInputProps { maxAmount: number; amount: string; - collateralBank?: ExtendedBankInfo; + collateralBank: ArenaBank | null; handleAmountChange: (value: string) => void; } @@ -24,9 +25,8 @@ export const AmountInput = ({ return (
- + {collateralBank?.meta.tokenSymbol.toUpperCase()} - {/* TODO: replace this with pool select */}
void; } diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx index b6492805e3..c9ab3aa023 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx @@ -79,7 +79,7 @@ export const InfoMessages = ({ actionMethods.concat(additionalChecks ?? []).map( (actionMethod, idx) => actionMethod.description && ( -
+
{ + const strippedAmount = amountRaw.replace(/,/g, ""); + return isNaN(Number.parseFloat(strippedAmount)) ? 0 : Number.parseFloat(strippedAmount); + }, [amountRaw]); + + const debouncedAmount = useAmountDebounce(amount, 500); + + const walletAmount = React.useMemo( + () => + collateralBank?.info.state.mint?.equals && collateralBank?.info.state.mint?.equals(WSOL_MINT) + ? collateralBank?.userInfo.tokenAccount.balance + nativeSolBalance + : collateralBank?.userInfo.tokenAccount.balance, + [nativeSolBalance, collateralBank] + ); + + const maxAmount = React.useMemo(() => { + if (!collateralBank) { + return 0; + } + + return collateralBank.userInfo.maxDeposit; + }, [collateralBank, activePool]); + + return { + amount, + debouncedAmount, + walletAmount, + maxAmount, + }; +} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts new file mode 100644 index 0000000000..7b242f3f53 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -0,0 +1,196 @@ +import { + computeMaxLeverage, + MarginfiAccountWrapper, + MarginfiClient, + SimulationResult, +} from "@mrgnlabs/marginfi-client-v2"; +import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; +import { SolanaTransaction } from "@mrgnlabs/mrgn-common"; +import { + ActionMessageType, + DYNAMIC_SIMULATION_ERRORS, + LoopActionTxns, + STATIC_SIMULATION_ERRORS, + usePrevious, +} from "@mrgnlabs/mrgn-utils"; +import { Transaction, VersionedTransaction } from "@solana/web3.js"; +import React from "react"; +import { calculateLooping } from "~/components/action-box-v2/actions/loop-box/utils/loop-action.utils"; +import { SimulationStatus } from "~/components/action-box-v2/utils"; +import { ArenaBank, ArenaPoolV2Extended } from "~/store/tradeStoreV2"; +import { calculateSummary, getSimulationResult } from "../utils"; + +export type TradeSimulationProps = { + debouncedAmount: number; + debouncedLeverage: number; + selectedBank: ArenaBank | null; + selectedSecondaryBank: ArenaBank | null; + marginfiClient: MarginfiClient | null; + actionTxns: LoopActionTxns | null; + simulationResult: SimulationResult | null; + wrappedAccount: MarginfiAccountWrapper | null; + accountSummary?: AccountSummary; + + slippageBps: number; + platformFeeBps: number; + + setActionTxns: (actionTxns: LoopActionTxns | null) => void; + setErrorMessage: (error: ActionMessageType | null) => void; + setIsLoading: ({ isLoading, status }: { isLoading: boolean; status: SimulationStatus }) => void; + setSimulationResult: (result: SimulationResult | null) => void; + setMaxLeverage: (maxLeverage: number) => void; +}; + +export function useTradeSimulation({ + debouncedAmount, + debouncedLeverage, + selectedBank, + selectedSecondaryBank, + marginfiClient, + wrappedAccount, + actionTxns, + simulationResult, + slippageBps, + platformFeeBps, + accountSummary, + + setActionTxns, + setErrorMessage, + setIsLoading, + setSimulationResult, + setMaxLeverage, +}: TradeSimulationProps) { + const prevDebouncedAmount = usePrevious(debouncedAmount); + const prevDebouncedLeverage = usePrevious(debouncedLeverage); + const prevSelectedSecondaryBank = usePrevious(selectedSecondaryBank); + + const handleSimulation = React.useCallback( + async (txns: (VersionedTransaction | Transaction)[]) => { + try { + setIsLoading({ isLoading: true, status: SimulationStatus.SIMULATING }); + if (wrappedAccount && selectedBank && txns.length > 0) { + const simulationResult = await getSimulationResult({ + account: wrappedAccount, + bank: selectedBank, + txns, + }); + if (simulationResult.actionMethod) { + setErrorMessage(simulationResult.actionMethod); + throw new Error(simulationResult.actionMethod.description); + } else { + setErrorMessage(null); + setSimulationResult(simulationResult.simulationResult); + } + } else { + throw new Error("account, bank or transactions are null"); + } + } catch (error) { + console.error("Error simulating transaction", error); + setSimulationResult(null); + } finally { + setIsLoading({ isLoading: false, status: SimulationStatus.COMPLETE }); + } + }, + [selectedBank, wrappedAccount, setErrorMessage, setIsLoading, setSimulationResult] + ); + + const handleActionSummary = React.useCallback( + (summary?: AccountSummary, result?: SimulationResult) => { + if (wrappedAccount && summary && selectedBank && actionTxns) { + return calculateSummary({ + simulationResult: result ?? undefined, + bank: selectedBank, + accountSummary: summary, + actionTxns: actionTxns, + }); + } + }, + [selectedBank, wrappedAccount, actionTxns] + ); + + const fetchTradeTxns = React.useCallback(async (amount: number, leverage: number) => { + if (amount === 0 || leverage === 0 || !selectedBank || !selectedSecondaryBank || !marginfiClient) { + setActionTxns(null); + setSimulationResult(null); + return; + } + + setIsLoading({ isLoading: true, status: SimulationStatus.PREPARING }); + + try { + const loopingResult = await calculateLooping({ + marginfiClient: marginfiClient, + marginfiAccount: wrappedAccount, + depositBank: selectedBank, + borrowBank: selectedSecondaryBank, + targetLeverage: leverage, + depositAmount: amount, + slippageBps: slippageBps, + connection: marginfiClient?.provider.connection, + platformFeeBps: platformFeeBps, + }); + + if (loopingResult && "actionQuote" in loopingResult) { + setActionTxns(loopingResult); + } else { + const errorMessage = + loopingResult ?? DYNAMIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED_CHECK(selectedSecondaryBank.meta.tokenSymbol); + // TODO: update + + setErrorMessage(errorMessage); + console.error("Error building looping transaction: ", errorMessage.description); + setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); + } + } catch (error) { + console.error("Error building looping transaction:", error); + setErrorMessage(STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED); // TODO: update + setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); + } + }, []); + + const fetchMaxLeverage = React.useCallback(async () => { + if (selectedBank && selectedSecondaryBank) { + const { maxLeverage, ltv } = computeMaxLeverage(selectedBank.info.rawBank, selectedSecondaryBank.info.rawBank); + + if (!maxLeverage) { + const errorMessage = DYNAMIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED_CHECK( + selectedSecondaryBank.meta.tokenSymbol + ); + setErrorMessage(errorMessage); + } else { + setMaxLeverage(maxLeverage); + } + } + }, [selectedBank, selectedSecondaryBank, setErrorMessage, setMaxLeverage]); + + React.useEffect(() => { + if (prevDebouncedAmount !== debouncedAmount || prevDebouncedLeverage !== debouncedLeverage) { + // Only set to PREPARING if we're actually going to simulate + if (debouncedAmount > 0) { + fetchTradeTxns(debouncedAmount, debouncedLeverage); + } + } + }, [debouncedAmount, debouncedLeverage, fetchTradeTxns, prevDebouncedAmount, prevDebouncedLeverage]); + + const actionSummary = React.useMemo(() => { + return handleActionSummary(accountSummary, simulationResult ?? undefined); + }, [accountSummary, simulationResult, handleActionSummary]); + + const refreshSimulation = React.useCallback(async () => { + await fetchTradeTxns(debouncedAmount ?? 0, debouncedLeverage ?? 0); + }, [fetchTradeTxns, debouncedAmount, debouncedLeverage]); + + // Fetch max leverage based when the secondary bank changes + // Not needed rn i think but when we do pay with any token it will be needed + React.useEffect(() => { + if (!selectedSecondaryBank) { + return; + } + const hasBankChanged = !prevSelectedSecondaryBank?.address.equals(selectedSecondaryBank.address); + if (hasBankChanged) { + fetchMaxLeverage(); + } + }, [selectedSecondaryBank, prevSelectedSecondaryBank, fetchMaxLeverage]); + + return { actionSummary, refreshSimulation }; +} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/index.ts new file mode 100644 index 0000000000..87831830a7 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/index.ts @@ -0,0 +1,5 @@ +import { createTradeBoxStore } from "./trade-box-store"; + +const useTradeBoxStore = createTradeBoxStore(); + +export { useTradeBoxStore }; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx new file mode 100644 index 0000000000..d1c1c14296 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx @@ -0,0 +1,184 @@ +import { ActionMessageType, calculateLstYield, LSTS_SOLANA_COMPASS_MAP } from "@mrgnlabs/mrgn-utils"; + +import { SimulationResult } from "@mrgnlabs/marginfi-client-v2"; +import { LoopActionTxns } from "@mrgnlabs/mrgn-utils"; +import { create, StateCreator } from "zustand"; +import { TradeSide } from ".."; +import { ArenaBank } from "~/store/tradeStoreV2"; + +interface TradeBoxState { + // State + amountRaw: string; + tradeState: TradeSide; + leverage: number; + maxLeverage: number; + + depositLstApy: number | null; + borrowLstApy: number | null; + + selectedBank: ArenaBank | null; + selectedSecondaryBank: ArenaBank | null; + + simulationResult: SimulationResult | null; + actionTxns: LoopActionTxns | null; + + errorMessage: ActionMessageType | null; + + // Actions + refreshState: () => void; + + setAmountRaw: (amountRaw: string, maxAmount?: number) => void; + setTradeState: (tradeState: TradeSide) => void; + setLeverage: (leverage: number) => void; + setSimulationResult: (result: SimulationResult | null) => void; + setActionTxns: (actionTxns: LoopActionTxns | null) => void; + setErrorMessage: (errorMessage: ActionMessageType | null) => void; + setSelectedBank: (bank: ArenaBank | null) => void; + setSelectedSecondaryBank: (bank: ArenaBank | null) => void; + setMaxLeverage: (maxLeverage: number) => void; + setDepositLstApy: (bank: ArenaBank) => void; + setBorrowLstApy: (bank: ArenaBank) => void; +} + +const initialState = { + amountRaw: "", + leverageAmount: 0, + leverage: 1, + simulationResult: null, + actionTxns: null, + errorMessage: null, + selectedBank: null, + selectedSecondaryBank: null, + maxLeverage: 0, + depositLstApy: null, + borrowLstApy: null, +}; + +function createTradeBoxStore() { + return create(stateCreator); +} + +const stateCreator: StateCreator = (set, get) => ({ + // State + ...initialState, + tradeState: "long" as TradeSide, + + refreshState() { + set(initialState); + }, + + setAmountRaw(amountRaw, maxAmount) { + const prevAmountRaw = get().amountRaw; + const isAmountChanged = amountRaw !== prevAmountRaw; + + if (isAmountChanged) { + set({ + simulationResult: null, + actionTxns: initialState.actionTxns, + errorMessage: null, + }); + } + + if (!maxAmount) { + set({ amountRaw }); + } else { + const strippedAmount = amountRaw.replace(/,/g, ""); + const amount = isNaN(Number.parseFloat(strippedAmount)) ? 0 : Number.parseFloat(strippedAmount); + const numberFormatter = new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }); + + if (amount && amount > maxAmount) { + set({ amountRaw: numberFormatter.format(maxAmount) }); + } else { + set({ amountRaw: numberFormatter.format(amount) }); + } + } + }, + + setTradeState(tradeState: TradeSide) { + set({ tradeState }); + }, + + setLeverage(leverage: number) { + set({ leverage }); + }, + + setSimulationResult(result: SimulationResult | null) { + set({ simulationResult: result }); + }, + + setActionTxns(actionTxns: LoopActionTxns | null) { + set({ actionTxns: actionTxns }); + }, + + setErrorMessage(errorMessage: ActionMessageType | null) { + set({ errorMessage: errorMessage }); + }, + + setSelectedBank(tokenBank) { + const selectedBank = get().selectedBank; + const hasBankChanged = !tokenBank || !selectedBank || !tokenBank.address.equals(selectedBank.address); + + if (hasBankChanged) { + if (tokenBank) { + get().setDepositLstApy(tokenBank); + } + set({ + selectedBank: tokenBank, + amountRaw: initialState.amountRaw, + leverage: initialState.leverage, + actionTxns: initialState.actionTxns, + errorMessage: null, + }); + } + }, + + setSelectedSecondaryBank(secondaryBank) { + const selectedSecondaryBank = get().selectedSecondaryBank; + const hasBankChanged = + !secondaryBank || !selectedSecondaryBank || !secondaryBank.address.equals(selectedSecondaryBank.address); + + if (hasBankChanged) { + if (secondaryBank) { + get().setBorrowLstApy(secondaryBank); + } + set({ + selectedSecondaryBank: secondaryBank, + amountRaw: initialState.amountRaw, + leverage: initialState.leverage, + actionTxns: initialState.actionTxns, + errorMessage: null, + }); + } else { + set({ selectedSecondaryBank: secondaryBank }); + } + }, + + setMaxLeverage(maxLeverage) { + set({ maxLeverage }); + }, + + async setDepositLstApy(bank: ArenaBank) { + const lstsArr = Object.keys(LSTS_SOLANA_COMPASS_MAP); + if (!lstsArr.includes(bank.meta.tokenSymbol)) { + set({ depositLstApy: null }); + return; + } else { + const depositLstApy = await calculateLstYield(bank); + set({ depositLstApy }); + } + }, + + async setBorrowLstApy(bank: ArenaBank) { + const lstsArr = Object.keys(LSTS_SOLANA_COMPASS_MAP); + if (!lstsArr.includes(bank.meta.tokenSymbol)) { + set({ borrowLstApy: null }); + return; + } else { + const borrowLstApy = await calculateLstYield(bank); + set({ borrowLstApy }); + } + }, +}); + +export { createTradeBoxStore }; +export type { TradeBoxState }; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 2ff4b71d38..6b2e096e5b 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -15,10 +15,14 @@ import { useTradeStoreV2, useUiStore } from "~/store"; import { IconSettings } from "@tabler/icons-react"; import { InfoMessages } from "./components/info-messages/info-messages"; import { useWallet, useWalletStore } from "~/components/wallet-v2"; -import { checkLoopingActionAvailable } from "../TradingBox/tradingBox.utils"; import { useExtendedPool } from "~/hooks/useExtendedPools"; import { useMarginfiClient } from "~/hooks/useMarginfiClient"; import { useWrappedAccount } from "~/hooks/useWrappedAccount"; +import { useTradeSimulation, useActionAmounts } from "./hooks"; +import { SimulationStatus } from "~/components/action-box-v2/utils"; +import { useAmountDebounce } from "~/hooks/useAmountDebounce"; +import { useTradeBoxStore } from "./store"; +import { checkLoopActionAvailable } from "./utils"; interface TradeBoxV2Props { activePool: ArenaPoolV2; @@ -26,6 +30,47 @@ interface TradeBoxV2Props { } export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { + const [ + amountRaw, + tradeState, + leverage, + simulationResult, + actionTxns, + errorMessage, + selectedBank, + selectedSecondaryBank, + maxLeverage, + refreshState, + setAmountRaw, + setTradeState, + setLeverage, + setSimulationResult, + setActionTxns, + setErrorMessage, + setSelectedBank, + setSelectedSecondaryBank, + setMaxLeverage, + ] = useTradeBoxStore((state) => [ + state.amountRaw, + state.tradeState, + state.leverage, + state.simulationResult, + state.actionTxns, + state.errorMessage, + state.selectedBank, + state.selectedSecondaryBank, + state.maxLeverage, + state.refreshState, + state.setAmountRaw, + state.setTradeState, + state.setLeverage, + state.setSimulationResult, + state.setActionTxns, + state.setErrorMessage, + state.setSelectedBank, + state.setSelectedSecondaryBank, + state.setMaxLeverage, + ]); // TODO: figure out amount vs amountRaw, ask kobe const activePoolExtended = useExtendedPool(activePool); const client = useMarginfiClient({ groupPk: activePoolExtended.groupPk }); const { accountSummary, wrappedAccount } = useWrappedAccount({ @@ -34,7 +79,11 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { banks: [activePoolExtended.tokenBank, activePoolExtended.quoteBank], }); const { walletContextState, wallet, connected } = useWallet(); - const [slippageBps, setSlippageBps] = useUiStore((state) => [state.slippageBps, state.setSlippageBps]); + const [slippageBps, setSlippageBps, platformFeeBps] = useUiStore((state) => [ + state.slippageBps, + state.setSlippageBps, + state.platformFeeBps, + ]); const [setIsWalletOpen] = useWalletStore((state) => [state.setIsWalletOpen]); const [fetchTradeState, nativeSolBalance, setIsRefreshingStore, refreshGroup] = useTradeStoreV2((state) => [ state.fetchTradeState, @@ -44,89 +93,96 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { ]); const { connection } = useConnection(); - const [tradeState, setTradeState] = React.useState(side); - const [leverage, setLeverage] = React.useState(0); - const [amount, setAmount] = React.useState(""); // use store for this maybe - const [tradeActionTxns, setTradeActionTxns] = React.useState(null); const [additionalChecks, setAdditionalChecks] = React.useState(); - const prevTradeState = usePrevious(tradeState); - - React.useEffect(() => { - if (tradeState !== prevTradeState) { - clearStates(); - } - }, [tradeState, prevTradeState]); - const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); // The fuck is this lol? - const leveragedAmount = React.useMemo(() => { - if (tradeState === "long") { - return tradeActionTxns?.actualDepositAmount; - } else { - return tradeActionTxns?.borrowAmount.toNumber(); - } - }, [tradeState, tradeActionTxns]); - - const collateralBank = React.useMemo(() => { + React.useEffect(() => { if (activePoolExtended) { if (tradeState === "short") { - return activePoolExtended.quoteBank; + setSelectedBank(activePoolExtended.quoteBank); + setSelectedSecondaryBank(activePoolExtended.tokenBank); } else { - return activePoolExtended.tokenBank; + setSelectedBank(activePoolExtended.tokenBank); + setSelectedSecondaryBank(activePoolExtended.quoteBank); } } }, [activePoolExtended, tradeState]); - const maxLeverage = React.useMemo(() => { - if (activePoolExtended) { - const deposit = - tradeState === "long" ? activePoolExtended.tokenBank.info.rawBank : activePoolExtended.quoteBank.info.rawBank; - const borrow = - tradeState === "long" ? activePoolExtended.quoteBank.info.rawBank : activePoolExtended.tokenBank.info.rawBank; + const { amount, debouncedAmount, walletAmount, maxAmount } = useActionAmounts({ + amountRaw, + activePool: activePoolExtended, + collateralBank: selectedBank, + nativeSolBalance, + }); - const { maxLeverage } = computeMaxLeverage(deposit, borrow); - return maxLeverage; - } - return 0; - }, [activePoolExtended, tradeState]); + const debouncedLeverage = useAmountDebounce(leverage, 500); - const isActiveWithCollat = true; // the fuuuuck? + // Loading states + const [isTransactionExecuting, setIsTransactionExecuting] = React.useState(false); + const [isSimulating, setIsSimulating] = React.useState<{ + isLoading: boolean; + status: SimulationStatus; + }>({ + isLoading: false, + status: SimulationStatus.IDLE, + }); + const isLoading = React.useMemo( + () => isTransactionExecuting || isSimulating.isLoading, + [isTransactionExecuting, isSimulating.isLoading] + ); - const maxAmount = React.useMemo(() => { - if (collateralBank) { - return collateralBank.userInfo.maxDeposit; + const { actionSummary, refreshSimulation } = useTradeSimulation({ + debouncedAmount: debouncedAmount ?? 0, + debouncedLeverage: debouncedLeverage ?? 0, + selectedBank: selectedBank, + selectedSecondaryBank: selectedSecondaryBank, + marginfiClient: client, + wrappedAccount: wrappedAccount, + slippageBps: slippageBps, + platformFeeBps: platformFeeBps, + actionTxns: actionTxns, + simulationResult: null, + accountSummary: accountSummary ?? undefined, + setActionTxns: setActionTxns, + setErrorMessage: setErrorMessage, + setIsLoading: setIsSimulating, + setSimulationResult: () => {}, + setMaxLeverage, + }); + + const leveragedAmount = React.useMemo(() => { + if (tradeState === "long") { + return actionTxns?.actualDepositAmount; + } else { + return actionTxns?.borrowAmount.toNumber(); } - return 0; - }, [collateralBank]); // Remove this and just use the collateralBank.userInfo.maxDeposit? + }, [tradeState, actionTxns]); + + const isActiveWithCollat = true; // the fuuuuck? const actionMethods = React.useMemo( () => - checkLoopingActionAvailable({ + checkLoopActionAvailable({ amount, connected, - activePoolExtended, - loopActionTxns: tradeActionTxns, - tradeSide: tradeState, - }), // TODO: do we need a more tailored check for trading instead of looping? - [amount, connected, activePoolExtended, tradeActionTxns, tradeState] + collateralBank: selectedBank, + secondaryBank: tradeState === "long" ? activePoolExtended.quoteBank : activePoolExtended.tokenBank, + // TODO: fix this, have the collateralBank and secondary be in the same var + actionQuote: null, + }), + + [amount, connected, activePoolExtended, actionTxns, tradeState] ); const handleAmountChange = React.useCallback( (amountRaw: string) => { - const amount = formatAmount(amountRaw, maxAmount, collateralBank ?? null, numberFormater); - setAmount(amount); + const amount = formatAmount(amountRaw, maxAmount, selectedBank ?? null, numberFormater); + setAmountRaw(amount); }, - [maxAmount, collateralBank, numberFormater] + [maxAmount, selectedBank, numberFormater] ); - const clearStates = () => { - setAmount(""); - setTradeActionTxns(null); - setLeverage(1); - setAdditionalChecks(undefined); - }; - return ( @@ -137,9 +193,9 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => {
@@ -147,7 +203,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { {`${ leveragedAmount ? leveragedAmount.toFixed(activePoolExtended.tokenBank.info.state.mintDecimals) : 0 - } ${collateralBank?.meta.tokenSymbol}`} + } ${selectedBank?.meta.tokenSymbol}`}
{actionMethods && actionMethods.some((method) => method.description) && ( @@ -182,7 +238,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { activePool={activePoolExtended} accountSummary={accountSummary} simulationResult={null} - actionTxns={tradeActionTxns} + actionTxns={actionTxns} />
diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts index f7cafa793c..4ebcd9d9ab 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts @@ -1 +1,3 @@ export * from "./trade-box.consts"; +export * from "./trade-box.utils"; +export * from "./trade-simulation.utils"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts index e69de29bb2..2b62f21b93 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts @@ -0,0 +1,104 @@ +import { QuoteResponse } from "@jup-ag/react-hook"; +import { OperationalState } from "@mrgnlabs/marginfi-client-v2"; +import { ActionMessageType, DYNAMIC_SIMULATION_ERRORS, isBankOracleStale } from "@mrgnlabs/mrgn-utils"; +import { ArenaBank } from "~/store/tradeStoreV2"; + +interface CheckLoopActionAvailableProps { + amount: number | null; + connected: boolean; + collateralBank: ArenaBank | null; + secondaryBank: ArenaBank | null; + actionQuote: QuoteResponse | null; +} + +export function checkLoopActionAvailable({ + amount, + connected, + collateralBank, + secondaryBank, + actionQuote, +}: CheckLoopActionAvailableProps): ActionMessageType[] { + let checks: ActionMessageType[] = []; + + const requiredCheck = getRequiredCheck(connected, collateralBank); + if (requiredCheck) return [requiredCheck]; + + const generalChecks = getGeneralChecks(amount ?? 0); + if (generalChecks) checks.push(...generalChecks); + + // allert checks + if (collateralBank) { + const loopChecks = canBeLooped(collateralBank, secondaryBank, actionQuote); + if (loopChecks.length) checks.push(...loopChecks); + } + + if (checks.length === 0) + checks.push({ + isEnabled: true, + }); + + return checks; +} + +function getRequiredCheck(connected: boolean, selectedBank: ArenaBank | null): ActionMessageType | null { + if (!connected) { + return { isEnabled: false }; + } + if (!selectedBank) { + return { isEnabled: false }; + } + + return null; +} + +function getGeneralChecks(amount: number = 0, showCloseBalance?: boolean): ActionMessageType[] { + let checks: ActionMessageType[] = []; + if (showCloseBalance) { + checks.push({ actionMethod: "INFO", description: "Close lending balance.", isEnabled: true }); + } // TODO: only for lend and withdraw + + if (amount === 0) { + checks.push({ isEnabled: false }); + } + + return checks; +} + +function canBeLooped( + targetBankInfo: ArenaBank, + repayBankInfo: ArenaBank | null, + swapQuote: QuoteResponse | null +): ActionMessageType[] { + let checks: ActionMessageType[] = []; + const isTargetBankPaused = targetBankInfo.info.rawBank.config.operationalState === OperationalState.Paused; + const isRepayBankPaused = repayBankInfo?.info.rawBank.config.operationalState === OperationalState.Paused; + + if (isTargetBankPaused || isRepayBankPaused) { + checks.push( + DYNAMIC_SIMULATION_ERRORS.BANK_PAUSED_CHECK( + isTargetBankPaused ? targetBankInfo.info.rawBank.tokenSymbol : repayBankInfo?.info.rawBank.tokenSymbol + ) + ); + } + + if (!swapQuote) { + checks.push({ + isEnabled: false, + }); + } + + if (swapQuote?.priceImpactPct && Number(swapQuote.priceImpactPct) > 0.01) { + //invert + if (swapQuote?.priceImpactPct && Number(swapQuote.priceImpactPct) > 0.05) { + checks.push(DYNAMIC_SIMULATION_ERRORS.PRICE_IMPACT_ERROR_CHECK(Number(swapQuote.priceImpactPct))); + } else { + checks.push(DYNAMIC_SIMULATION_ERRORS.PRICE_IMPACT_WARNING_CHECK(Number(swapQuote.priceImpactPct))); + } + } + + if ((repayBankInfo && isBankOracleStale(repayBankInfo)) || (targetBankInfo && isBankOracleStale(targetBankInfo))) { + checks.push(DYNAMIC_SIMULATION_ERRORS.STALE_CHECK("Looping")); + } + + return checks; +} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts new file mode 100644 index 0000000000..57c0dbafba --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts @@ -0,0 +1,119 @@ +import { SimulationResult } from "@mrgnlabs/marginfi-client-v2"; + +import { ActionMessageType, handleSimulationError, LoopActionTxns } from "@mrgnlabs/mrgn-utils"; + +import { SimulateActionProps } from "~/components/action-box-v2/actions/loop-box/utils"; // TODO: fix this import +import { ArenaBank } from "~/store/tradeStoreV2"; +import { + ActionPreview, + ActionSummary, + simulatedCollateral, + simulatedHealthFactor, + simulatedPositionSize, +} from "~/components/action-box-v2/utils"; +import { SimulatedActionPreview } from "~/components/action-box-v2/actions/lend-box/utils"; +import { nativeToUi } from "@mrgnlabs/mrgn-common"; +import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; + +export const getSimulationResult = async (props: SimulateActionProps) => { + let actionMethod: ActionMessageType | undefined = undefined; + let simulationResult: SimulationResult | null = null; + + try { + simulationResult = await simulateFlashLoan(props); + } catch (error: any) { + const actionString = "Looping"; + actionMethod = handleSimulationError(error, props.bank, false, actionString); + } + + return { simulationResult, actionMethod }; +}; + +async function simulateFlashLoan({ account, bank, txns }: SimulateActionProps) { + let simulationResult: SimulationResult; + + if (txns.length > 0) { + // todo: should we not inspect multiple banks? + simulationResult = await account.simulateBorrowLendTransaction(txns, [bank.address]); + return simulationResult; + } else { + console.error("Failed to simulate flashloan"); + throw new Error("Failed to simulate flashloan"); + } +} + +export function calculateSummary({ + simulationResult, + bank, + accountSummary, + actionTxns, +}: { + simulationResult?: SimulationResult; + bank: ArenaBank; + accountSummary: AccountSummary; + actionTxns: LoopActionTxns; +}): ActionSummary { + let simulationPreview: SimulatedActionPreview | null = null; + + if (simulationResult) { + simulationPreview = calculateSimulatedActionPreview(simulationResult, bank); + } + + const actionPreview = calculateActionPreview(bank, accountSummary, actionTxns); + + return { + actionPreview, + simulationPreview, + } as ActionSummary; +} + +export function calculateSimulatedActionPreview( + simulationResult: SimulationResult, + bank: ArenaBank +): SimulatedActionPreview { + const health = simulatedHealthFactor(simulationResult); + const positionAmount = simulatedPositionSize(simulationResult, bank); + const availableCollateral = simulatedCollateral(simulationResult); + + const liquidationPrice = simulationResult.marginfiAccount.computeLiquidationPriceForBank(bank.address); + const { lendingRate, borrowingRate } = simulationResult.banks.get(bank.address.toBase58())!.computeInterestRates(); + + return { + health, + liquidationPrice, + depositRate: lendingRate.toNumber(), + borrowRate: borrowingRate.toNumber(), + positionAmount, + availableCollateral, + }; +} + +function calculateActionPreview( + bank: ArenaBank, + accountSummary: AccountSummary, + actionTxns: LoopActionTxns +): ActionPreview { + const positionAmount = bank?.isActive ? bank.position.amount : 0; + const health = accountSummary.balance && accountSummary.healthFactor ? accountSummary.healthFactor : 1; + const liquidationPrice = + bank.isActive && bank.position.liquidationPrice && bank.position.liquidationPrice > 0.01 + ? bank.position.liquidationPrice + : null; + + const bankCap = nativeToUi( + false ? bank.info.rawBank.config.depositLimit : bank.info.rawBank.config.borrowLimit, + bank.info.state.mintDecimals + ); + + const priceImpactPct = actionTxns.actionQuote?.priceImpactPct; + const slippageBps = actionTxns.actionQuote?.slippageBps; + + return { + positionAmount, + health, + liquidationPrice, + bankCap, + priceImpactPct, + slippageBps, + } as ActionPreview; +} diff --git a/apps/marginfi-v2-trading/src/pages/trade/[symbol].tsx b/apps/marginfi-v2-trading/src/pages/trade/[symbol].tsx index 1e3dec560e..66d733bd41 100644 --- a/apps/marginfi-v2-trading/src/pages/trade/[symbol].tsx +++ b/apps/marginfi-v2-trading/src/pages/trade/[symbol].tsx @@ -84,12 +84,12 @@ export default function TradeSymbolPage({ initialData }: StaticArenaProps) {
- {/*
- -
*/} -
- +
+ {" "}
+ {/*
+ +
*/}
From 0ab395dbee8e1a565c3ad468f59d2f6b531b4143 Mon Sep 17 00:00:00 2001 From: borcherd Date: Thu, 12 Dec 2024 10:11:15 +0100 Subject: [PATCH 06/27] feat: various ui fixes & simulation finetuning --- .../action-button/action-button.tsx | 38 ++++ .../components/action-button/index.ts | 1 + .../amount-preview/amount-preview.tsx | 55 ++++++ .../components/amount-preview/index.ts | 1 + .../common/trade-box-v2/components/index.ts | 4 +- .../info-messages/info-messages.tsx | 163 ++++++++------- .../leverage-slider/leverage-slider.tsx | 89 ++++++--- .../hooks/use-trade-simulation.ts | 130 ++++++++---- .../trade-box-v2/store/trade-box-store.tsx | 28 ++- .../common/trade-box-v2/trade-box.tsx | 185 ++++++++++++------ .../trade-box-v2/utils/trade-box.utils.ts | 18 +- packages/mrgn-utils/src/errors.ts | 2 +- 12 files changed, 503 insertions(+), 211 deletions(-) create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-button/action-button.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-button/index.ts create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/index.ts diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-button/action-button.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-button/action-button.tsx new file mode 100644 index 0000000000..ac442124c1 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-button/action-button.tsx @@ -0,0 +1,38 @@ +import { IconLoader2 } from "@tabler/icons-react"; +import React from "react"; + +import { WalletButton } from "~/components/wallet-v2"; +import { Button } from "~/components/ui/button"; +import { cn } from "@mrgnlabs/mrgn-utils"; + +type ActionButtonProps = { + isLoading: boolean; + isEnabled: boolean; + buttonLabel: string; + connected?: boolean; + handleAction: () => void; + tradeState: "long" | "short"; +}; + +export const ActionButton = ({ + isLoading, + isEnabled, + buttonLabel, + connected = false, + handleAction, + tradeState, +}: ActionButtonProps) => { + if (!connected) { + return ; + } + + return ( + + ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-button/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-button/index.ts new file mode 100644 index 0000000000..526e8cc592 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-button/index.ts @@ -0,0 +1 @@ +export * from "./action-button"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx new file mode 100644 index 0000000000..655dc2ad5a --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx @@ -0,0 +1,55 @@ +import React from "react"; + +import { numeralFormatter } from "@mrgnlabs/mrgn-common"; +import { cn } from "@mrgnlabs/mrgn-utils"; + +import { IconLoader } from "~/components/ui/icons"; +import { ArenaBank } from "~/store/tradeStoreV2"; + +interface AmountPreviewProps { + tradeSide: "long" | "short"; + selectedBank: ArenaBank | null; + amount?: number; + isLoading?: boolean; +} + +export const AmountPreview = ({ tradeSide, amount, isLoading, selectedBank }: AmountPreviewProps) => { + return ( +
+
+ + {isLoading ? ( + + ) : amount ? ( + amount < 0.01 && amount > 0 ? ( + "< 0.01" + ) : ( + numeralFormatter(amount) + ) + ) : ( + "-" + )}{" "} + {/* TODO: get rid of the amount check above and use dynamicnumeralformatter */} + {selectedBank?.meta.tokenSymbol.toUpperCase()} + +
+
+ ); +}; + +interface StatProps { + label: string; + classNames?: string; + children: React.ReactNode; + style?: React.CSSProperties; +} +const Stat = ({ label, classNames, children, style }: StatProps) => { + return ( + <> +
{label}
+
+ {children} +
+ + ); +}; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/index.ts new file mode 100644 index 0000000000..e91118fa42 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/index.ts @@ -0,0 +1 @@ +export * from "./amount-preview"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts index 178c0d4604..b05639b03c 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts @@ -1,7 +1,9 @@ export * from "./action-toggle"; -export * from "./leverage-slider"; export * from "./amount-input"; export * from "./header"; export * from "./info-messages"; export * from "./stats"; export * from "./settings"; +export * from "./amount-preview"; +export * from "./action-button"; +export * from "./leverage-slider"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx index c9ab3aa023..f7c0ce1c2a 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx @@ -1,8 +1,8 @@ import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; import { Wallet } from "@mrgnlabs/mrgn-common"; -import { ActionMessageType } from "@mrgnlabs/mrgn-utils"; +import { ActionMessageType, cn } from "@mrgnlabs/mrgn-utils"; import { Connection } from "@solana/web3.js"; -import { IconAlertTriangle, IconExternalLink } from "@tabler/icons-react"; +import { IconAlertTriangle, IconExternalLink, IconLoader } from "@tabler/icons-react"; import Link from "next/link"; import { ActionBox } from "~/components"; import { Button } from "~/components/ui/button"; @@ -14,7 +14,7 @@ interface InfoMessagesProps { activePool: ArenaPoolV2Extended; isActiveWithCollat: boolean; actionMethods: ActionMessageType[]; - additionalChecks?: ActionMessageType; + additionalChecks?: ActionMessageType[]; setIsWalletOpen: (value: boolean) => void; fetchTradeState: ({ connection, @@ -25,8 +25,10 @@ interface InfoMessagesProps { wallet?: Wallet; refresh?: boolean; }) => Promise; + refreshSimulation: () => void; connection: any; wallet: any; + isRetrying?: boolean; } export const InfoMessages = ({ @@ -40,58 +42,99 @@ export const InfoMessages = ({ fetchTradeState, connection, wallet, + refreshSimulation, + isRetrying, }: InfoMessagesProps) => { - const renderLongWarning = () => ( -
+ const renderWarning = (message: string, action: () => void) => ( +
-
-

- You need to hold {activePool?.tokenBank.meta.tokenSymbol} to open a long position.{" "} - -

+
+

{message}

+
); - const renderShortWarning = () => ( -
- -
-

- You need to hold {activePool?.quoteBank.meta.tokenSymbol} to open a short position.{" "} - -

-
-
- ); + const renderLongWarning = () => + renderWarning(`You need to hold ${activePool?.tokenBank.meta.tokenSymbol} to open a long position.`, () => + setIsWalletOpen(true) + ); + + const renderShortWarning = () => + renderWarning(`You need to hold ${activePool?.quoteBank.meta.tokenSymbol} to open a short position.`, () => + setIsWalletOpen(true) + ); - const renderActionMethodMessages = () => - actionMethods.concat(additionalChecks ?? []).map( - (actionMethod, idx) => - actionMethod.description && ( -
+ const renderActionMethodMessages = () => ( +
+ {actionMethods.concat(additionalChecks ?? []).map( + (actionMethod, idx) => + actionMethod.description && (
-
-

{actionMethod.description}

+
+ {actionMethod.actionMethod !== "INFO" && ( +

+ {(actionMethod.actionMethod || "WARNING").toLowerCase()} +

+ )} +
+

{actionMethod.description}

+ {actionMethod.link && ( +

+ + {" "} + {actionMethod.linkText || "Read more"} + +

+ )} + {actionMethod.retry && refreshSimulation && ( + + )} +
{actionMethod.action && ( - ${actionMethod.action.type} + {actionMethod.action.type} ), title: `${actionMethod.action.type} ${actionMethod.action.bank.meta.tokenSymbol}`, }} /> )} - {actionMethod.link && ( -

- - {" "} - {actionMethod.linkText || "Read more"} - -

- )}
-
- ) - ); + ) + )} +
+ ); + + // TODO: currently, often two warning messages are shown. We should decide if we want to do that, or if we want to show only one. if we want to show only one, we should add a 'priority' or something to decide which one to show. const renderDepositCollateralDialog = () => ( { console.log("actionMethods", actionMethods); + console.log("additionalChecks", additionalChecks); if (!connected) return null; switch (true) { case tradeState === "long" && activePool?.tokenBank.userInfo.tokenAccount.balance === 0: - console.log("renderLongWarning"); return renderLongWarning(); case tradeState === "short" && activePool?.quoteBank.userInfo.tokenAccount.balance === 0: - console.log("renderShortWarning"); return renderShortWarning(); case isActiveWithCollat: - console.log("renderActionMethodMessages"); + console.log("isActiveWithCollat", isActiveWithCollat); return renderActionMethodMessages(); default: - console.log("renderDepositCollateralDialog"); return renderDepositCollateralDialog(); } }; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/leverage-slider.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/leverage-slider.tsx index debc280a8b..7e32a6bb09 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/leverage-slider.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/leverage-slider/leverage-slider.tsx @@ -1,31 +1,74 @@ -import { Label } from "~/components/ui/label"; +import React from "react"; + +import { cn } from "@mrgnlabs/mrgn-utils"; import { Slider } from "~/components/ui/slider"; +import { ArenaBank } from "~/store/tradeStoreV2"; -interface LeverageSliderProps { - leverage: number; +type LeverageSliderProps = { + selectedBank: ArenaBank | null; + selectedSecondaryBank: ArenaBank | null; + amountRaw: string; + leverageAmount: number; maxLeverage: number; - setLeverage: (value: number) => void; -} + setLeverageAmount: (amount: number) => void; +}; + +export const LeverageSlider = ({ + selectedBank, + selectedSecondaryBank, + amountRaw, + leverageAmount, + maxLeverage, + setLeverageAmount, +}: LeverageSliderProps) => { + const bothBanksSelected = React.useMemo( + () => Boolean(selectedBank && selectedSecondaryBank), + [selectedBank, selectedSecondaryBank] + ); -export const LeverageSlider = ({ leverage, maxLeverage, setLeverage }: LeverageSliderProps) => { return ( -
-
- - {leverage.toFixed(2)}x + <> +
+
+
+

Leverage

+
+ { + if (value[0] > maxLeverage || value[0] <= 1) return; + setLeverageAmount(value[0]); + }} + disabled={!bothBanksSelected || !amountRaw} + /> +
+

+ {leverageAmount > 1 && `${leverageAmount.toFixed(2)}x leverage`} +

+ + + {maxLeverage.toFixed(2)}x + + + +
+
- { - if (value[0] > maxLeverage) return; - setLeverage(value[0]); - }} - /> -
+ ); }; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index 7b242f3f53..0584712a3a 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -9,6 +9,7 @@ import { SolanaTransaction } from "@mrgnlabs/mrgn-common"; import { ActionMessageType, DYNAMIC_SIMULATION_ERRORS, + extractErrorString, LoopActionTxns, STATIC_SIMULATION_ERRORS, usePrevious, @@ -19,6 +20,7 @@ import { calculateLooping } from "~/components/action-box-v2/actions/loop-box/ut import { SimulationStatus } from "~/components/action-box-v2/utils"; import { ArenaBank, ArenaPoolV2Extended } from "~/store/tradeStoreV2"; import { calculateSummary, getSimulationResult } from "../utils"; +import BigNumber from "bignumber.js"; export type TradeSimulationProps = { debouncedAmount: number; @@ -26,7 +28,7 @@ export type TradeSimulationProps = { selectedBank: ArenaBank | null; selectedSecondaryBank: ArenaBank | null; marginfiClient: MarginfiClient | null; - actionTxns: LoopActionTxns | null; + actionTxns: LoopActionTxns; simulationResult: SimulationResult | null; wrappedAccount: MarginfiAccountWrapper | null; accountSummary?: AccountSummary; @@ -34,7 +36,7 @@ export type TradeSimulationProps = { slippageBps: number; platformFeeBps: number; - setActionTxns: (actionTxns: LoopActionTxns | null) => void; + setActionTxns: (actionTxns: LoopActionTxns) => void; setErrorMessage: (error: ActionMessageType | null) => void; setIsLoading: ({ isLoading, status }: { isLoading: boolean; status: SimulationStatus }) => void; setSimulationResult: (result: SimulationResult | null) => void; @@ -108,45 +110,72 @@ export function useTradeSimulation({ [selectedBank, wrappedAccount, actionTxns] ); - const fetchTradeTxns = React.useCallback(async (amount: number, leverage: number) => { - if (amount === 0 || leverage === 0 || !selectedBank || !selectedSecondaryBank || !marginfiClient) { - setActionTxns(null); - setSimulationResult(null); - return; - } + const fetchTradeTxns = React.useCallback( + async (amount: number, leverage: number) => { + if (amount === 0 || leverage === 0 || !selectedBank || !selectedSecondaryBank || !marginfiClient) { + setActionTxns({ + actionTxn: null, + additionalTxns: [], + actionQuote: null, + lastValidBlockHeight: undefined, + actualDepositAmount: 0, + borrowAmount: new BigNumber(0), + }); // TODO: create init state from this + setSimulationResult(null); + return; + } + + setIsLoading({ isLoading: true, status: SimulationStatus.PREPARING }); - setIsLoading({ isLoading: true, status: SimulationStatus.PREPARING }); - - try { - const loopingResult = await calculateLooping({ - marginfiClient: marginfiClient, - marginfiAccount: wrappedAccount, - depositBank: selectedBank, - borrowBank: selectedSecondaryBank, - targetLeverage: leverage, - depositAmount: amount, - slippageBps: slippageBps, - connection: marginfiClient?.provider.connection, - platformFeeBps: platformFeeBps, + console.log({ + selectedBank, + selectedSecondaryBank, }); - if (loopingResult && "actionQuote" in loopingResult) { - setActionTxns(loopingResult); - } else { - const errorMessage = - loopingResult ?? DYNAMIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED_CHECK(selectedSecondaryBank.meta.tokenSymbol); - // TODO: update + try { + const loopingResult = await calculateLooping({ + marginfiClient: marginfiClient, + marginfiAccount: wrappedAccount, + depositBank: selectedBank, + borrowBank: selectedSecondaryBank, + targetLeverage: leverage, + depositAmount: amount, + slippageBps: slippageBps, + connection: marginfiClient?.provider.connection, + platformFeeBps: platformFeeBps, + }); - setErrorMessage(errorMessage); - console.error("Error building looping transaction: ", errorMessage.description); + if (loopingResult && "actionQuote" in loopingResult) { + setActionTxns(loopingResult); + } else { + const errorMessage = + loopingResult ?? + DYNAMIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED_CHECK(selectedSecondaryBank.meta.tokenSymbol); + // TODO: update + + setErrorMessage(errorMessage); + console.error("Error building looping transaction: ", errorMessage.description); + setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); + } + } catch (error) { + console.error("Error building looping transaction:", error); + setErrorMessage(STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED); // TODO: update setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); } - } catch (error) { - console.error("Error building looping transaction:", error); - setErrorMessage(STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED); // TODO: update - setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); - } - }, []); + }, + [ + selectedBank, + selectedSecondaryBank, + marginfiClient, + wrappedAccount, + slippageBps, + platformFeeBps, + setErrorMessage, + setIsLoading, + setActionTxns, + setSimulationResult, + ] + ); const fetchMaxLeverage = React.useCallback(async () => { if (selectedBank && selectedSecondaryBank) { @@ -166,19 +195,26 @@ export function useTradeSimulation({ React.useEffect(() => { if (prevDebouncedAmount !== debouncedAmount || prevDebouncedLeverage !== debouncedLeverage) { // Only set to PREPARING if we're actually going to simulate - if (debouncedAmount > 0) { + if (debouncedAmount > 0 && debouncedLeverage > 0) { + console.log("fetching trade txns"); fetchTradeTxns(debouncedAmount, debouncedLeverage); } } }, [debouncedAmount, debouncedLeverage, fetchTradeTxns, prevDebouncedAmount, prevDebouncedLeverage]); - const actionSummary = React.useMemo(() => { - return handleActionSummary(accountSummary, simulationResult ?? undefined); - }, [accountSummary, simulationResult, handleActionSummary]); - - const refreshSimulation = React.useCallback(async () => { - await fetchTradeTxns(debouncedAmount ?? 0, debouncedLeverage ?? 0); - }, [fetchTradeTxns, debouncedAmount, debouncedLeverage]); + React.useEffect(() => { + // Only run simulation if we have transactions to simulate + if (actionTxns?.actionTxn || (actionTxns?.additionalTxns?.length ?? 0) > 0) { + handleSimulation([ + ...(actionTxns?.additionalTxns ?? []), + ...(actionTxns?.actionTxn ? [actionTxns?.actionTxn] : []), + ]); + } else { + // If no transactions, move back to idle state + setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionTxns]); // Fetch max leverage based when the secondary bank changes // Not needed rn i think but when we do pay with any token it will be needed @@ -192,5 +228,13 @@ export function useTradeSimulation({ } }, [selectedSecondaryBank, prevSelectedSecondaryBank, fetchMaxLeverage]); + const actionSummary = React.useMemo(() => { + return handleActionSummary(accountSummary, simulationResult ?? undefined); + }, [accountSummary, simulationResult, handleActionSummary]); + + const refreshSimulation = React.useCallback(async () => { + await fetchTradeTxns(debouncedAmount ?? 0, debouncedLeverage ?? 0); + }, [fetchTradeTxns, debouncedAmount, debouncedLeverage]); + return { actionSummary, refreshSimulation }; } diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx index d1c1c14296..4abee5bf7d 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx @@ -1,4 +1,5 @@ import { ActionMessageType, calculateLstYield, LSTS_SOLANA_COMPASS_MAP } from "@mrgnlabs/mrgn-utils"; +import BigNumber from "bignumber.js"; import { SimulationResult } from "@mrgnlabs/marginfi-client-v2"; import { LoopActionTxns } from "@mrgnlabs/mrgn-utils"; @@ -20,7 +21,7 @@ interface TradeBoxState { selectedSecondaryBank: ArenaBank | null; simulationResult: SimulationResult | null; - actionTxns: LoopActionTxns | null; + actionTxns: LoopActionTxns; errorMessage: ActionMessageType | null; @@ -31,7 +32,7 @@ interface TradeBoxState { setTradeState: (tradeState: TradeSide) => void; setLeverage: (leverage: number) => void; setSimulationResult: (result: SimulationResult | null) => void; - setActionTxns: (actionTxns: LoopActionTxns | null) => void; + setActionTxns: (actionTxns: LoopActionTxns) => void; setErrorMessage: (errorMessage: ActionMessageType | null) => void; setSelectedBank: (bank: ArenaBank | null) => void; setSelectedSecondaryBank: (bank: ArenaBank | null) => void; @@ -43,15 +44,23 @@ interface TradeBoxState { const initialState = { amountRaw: "", leverageAmount: 0, - leverage: 1, + leverage: 0, simulationResult: null, - actionTxns: null, errorMessage: null, selectedBank: null, selectedSecondaryBank: null, maxLeverage: 0, depositLstApy: null, borrowLstApy: null, + + actionTxns: { + actionTxn: null, + additionalTxns: [], + actionQuote: null, + lastValidBlockHeight: undefined, + actualDepositAmount: 0, + borrowAmount: new BigNumber(0), + }, }; function createTradeBoxStore() { @@ -64,7 +73,14 @@ const stateCreator: StateCreator = (set, get) => ({ tradeState: "long" as TradeSide, refreshState() { - set(initialState); + set({ + amountRaw: initialState.amountRaw, + leverage: initialState.leverage, + actionTxns: initialState.actionTxns, + errorMessage: null, + depositLstApy: initialState.depositLstApy, // TODO: can we remove? Not using anywhere + borrowLstApy: initialState.borrowLstApy, // TODO: can we remove? Not using anywhere + }); }, setAmountRaw(amountRaw, maxAmount) { @@ -106,7 +122,7 @@ const stateCreator: StateCreator = (set, get) => ({ set({ simulationResult: result }); }, - setActionTxns(actionTxns: LoopActionTxns | null) { + setActionTxns(actionTxns: LoopActionTxns) { set({ actionTxns: actionTxns }); }, diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 6b2e096e5b..f1fb43fbdf 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -1,28 +1,35 @@ "use client"; import React from "react"; -import { computeMaxLeverage } from "@mrgnlabs/marginfi-client-v2"; -import { ActionMessageType, cn, formatAmount, LoopActionTxns, useConnection, usePrevious } from "@mrgnlabs/mrgn-utils"; -// import { GroupData } from "~/store/tradeStore"; +import { ActionMessageType, formatAmount, showErrorToast, useConnection, usePrevious } from "@mrgnlabs/mrgn-utils"; +import { IconSettings } from "@tabler/icons-react"; + import { ArenaPoolV2 } from "~/store/tradeStoreV2"; import { TradeSide } from "~/components/common/trade-box-v2/utils"; -import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; - -import { ActionToggle, AmountInput, Header, LeverageSlider, Stats, TradingBoxSettingsDialog } from "./components"; import { useTradeStoreV2, useUiStore } from "~/store"; -import { IconSettings } from "@tabler/icons-react"; -import { InfoMessages } from "./components/info-messages/info-messages"; import { useWallet, useWalletStore } from "~/components/wallet-v2"; import { useExtendedPool } from "~/hooks/useExtendedPools"; import { useMarginfiClient } from "~/hooks/useMarginfiClient"; import { useWrappedAccount } from "~/hooks/useWrappedAccount"; -import { useTradeSimulation, useActionAmounts } from "./hooks"; import { SimulationStatus } from "~/components/action-box-v2/utils"; import { useAmountDebounce } from "~/hooks/useAmountDebounce"; + +import { + ActionButton, + ActionToggle, + AmountInput, + AmountPreview, + Header, + LeverageSlider, + Stats, + TradingBoxSettingsDialog, + InfoMessages, +} from "./components"; import { useTradeBoxStore } from "./store"; -import { checkLoopActionAvailable } from "./utils"; +import { checkTradeActionAvailable } from "./utils"; +import { useTradeSimulation, useActionAmounts } from "./hooks"; interface TradeBoxV2Props { activePool: ArenaPoolV2; @@ -30,6 +37,7 @@ interface TradeBoxV2Props { } export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { + // Stores const [ amountRaw, tradeState, @@ -70,15 +78,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { state.setSelectedBank, state.setSelectedSecondaryBank, state.setMaxLeverage, - ]); // TODO: figure out amount vs amountRaw, ask kobe - const activePoolExtended = useExtendedPool(activePool); - const client = useMarginfiClient({ groupPk: activePoolExtended.groupPk }); - const { accountSummary, wrappedAccount } = useWrappedAccount({ - client, - groupPk: activePoolExtended.groupPk, - banks: [activePoolExtended.tokenBank, activePoolExtended.quoteBank], - }); - const { walletContextState, wallet, connected } = useWallet(); + ]); const [slippageBps, setSlippageBps, platformFeeBps] = useUiStore((state) => [ state.slippageBps, state.setSlippageBps, @@ -91,33 +91,28 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { state.setIsRefreshingStore, state.refreshGroup, ]); - const { connection } = useConnection(); - - const [additionalChecks, setAdditionalChecks] = React.useState(); - - const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); // The fuck is this lol? - - React.useEffect(() => { - if (activePoolExtended) { - if (tradeState === "short") { - setSelectedBank(activePoolExtended.quoteBank); - setSelectedSecondaryBank(activePoolExtended.tokenBank); - } else { - setSelectedBank(activePoolExtended.tokenBank); - setSelectedSecondaryBank(activePoolExtended.quoteBank); - } - } - }, [activePoolExtended, tradeState]); + // Hooks + const activePoolExtended = useExtendedPool(activePool); + const client = useMarginfiClient({ groupPk: activePoolExtended.groupPk }); + const { accountSummary, wrappedAccount } = useWrappedAccount({ + client, + groupPk: activePoolExtended.groupPk, + banks: [activePoolExtended.tokenBank, activePoolExtended.quoteBank], + }); + const { walletContextState, wallet, connected } = useWallet(); + const { connection } = useConnection(); const { amount, debouncedAmount, walletAmount, maxAmount } = useActionAmounts({ amountRaw, activePool: activePoolExtended, collateralBank: selectedBank, nativeSolBalance, }); - const debouncedLeverage = useAmountDebounce(leverage, 500); + // States + const [additionalActionMessages, setAdditionalActionMessages] = React.useState([]); + // Loading states const [isTransactionExecuting, setIsTransactionExecuting] = React.useState(false); const [isSimulating, setIsSimulating] = React.useState<{ @@ -132,6 +127,44 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { [isTransactionExecuting, isSimulating.isLoading] ); + // Memos + const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); // The fuck is this lol? + + const leveragedAmount = React.useMemo(() => { + if (tradeState === "long") { + return actionTxns?.actualDepositAmount; + } else { + return actionTxns?.borrowAmount.toNumber(); + } + }, [tradeState, actionTxns]); + + // Effects + React.useEffect(() => { + if (activePoolExtended) { + if (tradeState === "short") { + setSelectedBank(activePoolExtended.quoteBank); + setSelectedSecondaryBank(activePoolExtended.tokenBank); + } else { + setSelectedBank(activePoolExtended.tokenBank); + setSelectedSecondaryBank(activePoolExtended.quoteBank); + } + } + }, [activePoolExtended, tradeState]); + + React.useEffect(() => { + if (errorMessage && errorMessage.description) { + showErrorToast(errorMessage?.description); + setAdditionalActionMessages([errorMessage]); + } else { + setAdditionalActionMessages([]); + } + }, [errorMessage]); + + // TODO: on load, reset everything + React.useEffect(() => { + refreshState(); + }, []); + const { actionSummary, refreshSimulation } = useTradeSimulation({ debouncedAmount: debouncedAmount ?? 0, debouncedLeverage: debouncedLeverage ?? 0, @@ -147,29 +180,42 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { setActionTxns: setActionTxns, setErrorMessage: setErrorMessage, setIsLoading: setIsSimulating, - setSimulationResult: () => {}, + setSimulationResult, setMaxLeverage, }); - const leveragedAmount = React.useMemo(() => { + React.useEffect(() => { + console.log({ + selectedBank: selectedBank?.meta.tokenSymbol, + selectedSecondaryBank: selectedSecondaryBank?.meta.tokenSymbol, + }); + + let borrowBank, depositBank; + if (tradeState === "long") { - return actionTxns?.actualDepositAmount; + depositBank = activePoolExtended.tokenBank; + borrowBank = activePoolExtended.quoteBank; } else { - return actionTxns?.borrowAmount.toNumber(); + depositBank = activePoolExtended.quoteBank; + borrowBank = activePoolExtended.tokenBank; } - }, [tradeState, actionTxns]); - const isActiveWithCollat = true; // the fuuuuck? + console.log({ + depositBank: depositBank?.meta.tokenSymbol, + borrowBank: borrowBank?.meta.tokenSymbol, + }); + }, [selectedBank, selectedSecondaryBank]); + + const isActiveWithCollat = true; // TODO: figure out what this does? const actionMethods = React.useMemo( () => - checkLoopActionAvailable({ + checkTradeActionAvailable({ amount, connected, collateralBank: selectedBank, - secondaryBank: tradeState === "long" ? activePoolExtended.quoteBank : activePoolExtended.tokenBank, - // TODO: fix this, have the collateralBank and secondary be in the same var - actionQuote: null, + secondaryBank: selectedSecondaryBank, + actionQuote: actionTxns.actionQuote, }), [amount, connected, activePoolExtended, actionTxns, tradeState] @@ -197,32 +243,47 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { handleAmountChange={handleAmountChange} collateralBank={selectedBank} /> - -
- Size of {tradeState} - - {`${ - leveragedAmount ? leveragedAmount.toFixed(activePoolExtended.tokenBank.info.state.mintDecimals) : 0 - } ${selectedBank?.meta.tokenSymbol}`} - -
- {actionMethods && actionMethods.some((method) => method.description) && ( + + + {actionMethods && actionMethods.concat(additionalActionMessages).some((method) => method.description) && ( )} - + + value.isEnabled === false).length + } + connected={connected} + handleAction={() => {}} + buttonLabel={tradeState === "long" ? "Long" : "Short"} + tradeState={tradeState} + /> setSlippageBps(value * 100)} slippageBps={slippageBps / 100} @@ -237,7 +298,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts index 2b62f21b93..518af41a65 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts @@ -1,9 +1,9 @@ -import { QuoteResponse } from "@jup-ag/react-hook"; +import { QuoteResponse } from "@jup-ag/api"; import { OperationalState } from "@mrgnlabs/marginfi-client-v2"; import { ActionMessageType, DYNAMIC_SIMULATION_ERRORS, isBankOracleStale } from "@mrgnlabs/mrgn-utils"; import { ArenaBank } from "~/store/tradeStoreV2"; -interface CheckLoopActionAvailableProps { +interface CheckTradeActionAvailableProps { amount: number | null; connected: boolean; collateralBank: ArenaBank | null; @@ -11,13 +11,13 @@ interface CheckLoopActionAvailableProps { actionQuote: QuoteResponse | null; } -export function checkLoopActionAvailable({ +export function checkTradeActionAvailable({ amount, connected, collateralBank, secondaryBank, actionQuote, -}: CheckLoopActionAvailableProps): ActionMessageType[] { +}: CheckTradeActionAvailableProps): ActionMessageType[] { let checks: ActionMessageType[] = []; const requiredCheck = getRequiredCheck(connected, collateralBank); @@ -28,8 +28,8 @@ export function checkLoopActionAvailable({ // allert checks if (collateralBank) { - const loopChecks = canBeLooped(collateralBank, secondaryBank, actionQuote); - if (loopChecks.length) checks.push(...loopChecks); + const tradeChecks = canBeTraded(collateralBank, secondaryBank, actionQuote); + if (tradeChecks.length) checks.push(...tradeChecks); } if (checks.length === 0) @@ -55,7 +55,7 @@ function getGeneralChecks(amount: number = 0, showCloseBalance?: boolean): Actio let checks: ActionMessageType[] = []; if (showCloseBalance) { checks.push({ actionMethod: "INFO", description: "Close lending balance.", isEnabled: true }); - } // TODO: only for lend and withdraw + } if (amount === 0) { checks.push({ isEnabled: false }); @@ -64,7 +64,7 @@ function getGeneralChecks(amount: number = 0, showCloseBalance?: boolean): Actio return checks; } -function canBeLooped( +function canBeTraded( targetBankInfo: ArenaBank, repayBankInfo: ArenaBank | null, swapQuote: QuoteResponse | null @@ -97,7 +97,7 @@ function canBeLooped( } if ((repayBankInfo && isBankOracleStale(repayBankInfo)) || (targetBankInfo && isBankOracleStale(targetBankInfo))) { - checks.push(DYNAMIC_SIMULATION_ERRORS.STALE_CHECK("Looping")); + checks.push(DYNAMIC_SIMULATION_ERRORS.STALE_CHECK("Trading")); } return checks; diff --git a/packages/mrgn-utils/src/errors.ts b/packages/mrgn-utils/src/errors.ts index 94f08446b5..c6960884b3 100644 --- a/packages/mrgn-utils/src/errors.ts +++ b/packages/mrgn-utils/src/errors.ts @@ -62,7 +62,7 @@ export const STATIC_SIMULATION_ERRORS: { [key: string]: ActionMessageType } = { actionMethod: "WARNING", description: "Transaction failed due to poor account health, please increase your collateral and try again.", code: 108, - }, + }, // We should add an action to deposit collateral here, this is quite often being thrown in the arena USER_REJECTED: { isEnabled: false, actionMethod: "WARNING", From 8c62a3ffc3a926cc428e437f398d58ebeba8a836 Mon Sep 17 00:00:00 2001 From: borcherd Date: Thu, 12 Dec 2024 10:32:17 +0100 Subject: [PATCH 07/27] feat: tx execution --- .../common/trade-box-v2/trade-box.tsx | 146 +++++++++++++++++- .../common/trade-box-v2/utils/index.ts | 1 + .../trade-box-v2/utils/trade-action.utils.ts | 54 +++++++ 3 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index f1fb43fbdf..51e7e2de4b 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -2,11 +2,22 @@ import React from "react"; -import { ActionMessageType, formatAmount, showErrorToast, useConnection, usePrevious } from "@mrgnlabs/mrgn-utils"; +import { + ActionMessageType, + ActionTxns, + ExecuteLoopingActionProps, + formatAmount, + IndividualFlowError, + MultiStepToastHandle, + PreviousTxn, + showErrorToast, + useConnection, + usePrevious, +} from "@mrgnlabs/mrgn-utils"; import { IconSettings } from "@tabler/icons-react"; import { ArenaPoolV2 } from "~/store/tradeStoreV2"; -import { TradeSide } from "~/components/common/trade-box-v2/utils"; +import { handleExecuteTradeAction, TradeSide } from "~/components/common/trade-box-v2/utils"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { useTradeStoreV2, useUiStore } from "~/store"; import { useWallet, useWalletStore } from "~/components/wallet-v2"; @@ -30,6 +41,9 @@ import { import { useTradeBoxStore } from "./store"; import { checkTradeActionAvailable } from "./utils"; import { useTradeSimulation, useActionAmounts } from "./hooks"; +import { ActionType, ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; +import { useActionContext } from "@mrgnlabs/mrgn-ui"; +import { UnderlineIcon } from "@radix-ui/react-icons"; interface TradeBoxV2Props { activePool: ArenaPoolV2; @@ -79,10 +93,12 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { state.setSelectedSecondaryBank, state.setMaxLeverage, ]); - const [slippageBps, setSlippageBps, platformFeeBps] = useUiStore((state) => [ + const [slippageBps, setSlippageBps, platformFeeBps, broadcastType, priorityFees] = useUiStore((state) => [ state.slippageBps, state.setSlippageBps, state.platformFeeBps, + state.broadcastType, + state.priorityFees, ]); const [setIsWalletOpen] = useWalletStore((state) => [state.setIsWalletOpen]); const [fetchTradeState, nativeSolBalance, setIsRefreshingStore, refreshGroup] = useTradeStoreV2((state) => [ @@ -229,6 +245,126 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { [maxAmount, selectedBank, numberFormater] ); + ///////////////////// + // Trading Actions // + ///////////////////// + const executeAction = async ( + params: ExecuteLoopingActionProps, + leverage: number, + callbacks: { + captureEvent?: (event: string, properties?: Record) => void; + setIsActionComplete: (isComplete: boolean) => void; + setPreviousTxn: (previousTxn: PreviousTxn) => void; + onComplete?: (txn: PreviousTxn) => void; + setIsLoading: (isLoading: boolean) => void; + setAmountRaw: (amountRaw: string) => void; + retryCallback: (txs: ActionTxns, toast: MultiStepToastHandle) => void; + } + ) => { + const action = async (params: ExecuteLoopingActionProps) => { + await handleExecuteTradeAction({ + props: params, + captureEvent: (event, properties) => { + callbacks.captureEvent && callbacks.captureEvent(event, properties); + }, + setIsComplete: (txnSigs) => { + callbacks.setIsActionComplete(true); + callbacks.setPreviousTxn({ + txn: txnSigs[txnSigs.length - 1] ?? "", + txnType: "LOOP", + loopOptions: { + depositBank: params.depositBank as ActiveBankInfo, + borrowBank: params.borrowBank as ActiveBankInfo, + depositAmount: params.actualDepositAmount, + borrowAmount: params.borrowAmount.toNumber(), + leverage: leverage, + }, + }); + + callbacks.onComplete && + callbacks.onComplete({ + txn: txnSigs[txnSigs.length - 1] ?? "", + txnType: "LEND", + lendingOptions: { + amount: params.depositAmount, + type: ActionType.Loop, + bank: params.depositBank as ActiveBankInfo, + }, + }); + }, + setError: (error: IndividualFlowError) => { + // TODO: update the messaging within the toast. Might need tailored functions in the sdk for trading + console.log("error", error); + const toast = error.multiStepToast as MultiStepToastHandle; // TODO: check if this works, not sure it does + const txs = error.actionTxns as ActionTxns; + let retry = undefined; + if (error.retry && toast && txs) { + retry = () => callbacks.retryCallback(txs, toast); + } + toast.setFailed(error.message, retry); + callbacks.setIsLoading(false); + }, + setIsLoading: (isLoading) => callbacks.setIsLoading(isLoading), + }); + }; + await action(params); + callbacks.setAmountRaw(""); + }; + + const retryTradeAction = React.useCallback( + (params: ExecuteLoopingActionProps, leverage: number) => { + executeAction(params, leverage, { + captureEvent: () => {}, // TODO: implement this + setIsActionComplete: () => {}, // TODO: implement this + setPreviousTxn: () => {}, // TODO: implement this + onComplete: () => {}, // TODO: implement this + setIsLoading: setIsTransactionExecuting, + setAmountRaw, + retryCallback: (txns: ActionTxns, multiStepToast: MultiStepToastHandle) => { + retryTradeAction({ ...params, actionTxns: txns, multiStepToast }, leverage); + }, + }); + }, + [setAmountRaw, setIsTransactionExecuting] + ); + + const handleTradeAction = React.useCallback(async () => { + if (!client || !selectedBank || !selectedSecondaryBank || !actionTxns) { + return; + } + + const params: ExecuteLoopingActionProps = { + marginfiClient: client, + actionTxns, + processOpts: { + ...priorityFees, + broadcastType, + }, + txOpts: {}, + + marginfiAccount: wrappedAccount, + depositAmount: amount, + borrowAmount: actionTxns.borrowAmount, + actualDepositAmount: actionTxns.actualDepositAmount, + depositBank: selectedBank, + borrowBank: selectedSecondaryBank, + quote: actionTxns.actionQuote!, + connection: client.provider.connection, + }; + + executeAction(params, leverage, { + captureEvent: () => {}, // TODO: implement this + setIsActionComplete: () => {}, // TODO: implement this + setPreviousTxn: () => {}, // TODO: implement this + onComplete: () => {}, // TODO: implement this + setIsLoading: setIsTransactionExecuting, + setAmountRaw, + retryCallback: (txns: ActionTxns, multiStepToast: MultiStepToastHandle) => { + retryTradeAction({ ...params, actionTxns: txns, multiStepToast }, leverage); + }, + }); + }, []); + return ( @@ -280,7 +416,9 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { !actionMethods.concat(additionalActionMessages).filter((value) => value.isEnabled === false).length } connected={connected} - handleAction={() => {}} + handleAction={() => { + handleTradeAction(); + }} buttonLabel={tradeState === "long" ? "Long" : "Short"} tradeState={tradeState} /> diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts index 4ebcd9d9ab..80d5ecd012 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/index.ts @@ -1,3 +1,4 @@ export * from "./trade-box.consts"; export * from "./trade-box.utils"; export * from "./trade-simulation.utils"; +export * from "./trade-action.utils"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts new file mode 100644 index 0000000000..093e6e39aa --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts @@ -0,0 +1,54 @@ +import { v4 as uuidv4 } from "uuid"; +import { + ActionMessageType, + calculateLoopingParams, + CalculateLoopingProps, + executeLoopingAction, + LoopActionTxns, + ExecuteLoopingActionProps, + IndividualFlowError, +} from "@mrgnlabs/mrgn-utils"; + +import { ExecuteActionsCallbackProps } from "~/components/action-box-v2/types"; + +interface ExecuteTradeActionsProps extends ExecuteActionsCallbackProps { + props: ExecuteLoopingActionProps; +} + +export const handleExecuteTradeAction = async ({ + props, + captureEvent, + setIsLoading, + setIsComplete, + setError, +}: ExecuteTradeActionsProps) => { + try { + setIsLoading(true); + const attemptUuid = uuidv4(); + captureEvent(`user_trade_initiate`, { + uuid: attemptUuid, + tokenSymbol: props.borrowBank.meta.tokenSymbol, + tokenName: props.borrowBank.meta.tokenName, + amount: props.depositAmount, + priorityFee: props.processOpts?.priorityFeeMicro ?? 0, + }); + + const txnSig = await executeLoopingAction(props); + + setIsLoading(false); + + if (txnSig) { + setIsComplete(Array.isArray(txnSig) ? txnSig : [txnSig]); + captureEvent(`user_trade`, { + uuid: attemptUuid, + tokenSymbol: props.borrowBank.meta.tokenSymbol, + tokenName: props.borrowBank.meta.tokenName, + amount: props.depositAmount, + txn: txnSig!, + priorityFee: props.processOpts?.priorityFeeMicro ?? 0, + }); + } + } catch (error) { + setError(error as IndividualFlowError); + } +}; From 9e1835b352734deaa331faba694ebfdae806cb53 Mon Sep 17 00:00:00 2001 From: borcherd Date: Thu, 12 Dec 2024 10:37:08 +0100 Subject: [PATCH 08/27] chore: remove logs --- .../action-toggle/action-toggle.tsx | 1 - .../hooks/use-trade-simulation.ts | 6 ----- .../common/trade-box-v2/trade-box.tsx | 22 ------------------- 3 files changed, 29 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx index 7e13b6178c..291af247ae 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx @@ -13,7 +13,6 @@ export const ActionToggle = ({ tradeState, setTradeState }: ActionToggleProps) = className="w-full gap-4" value={tradeState} onValueChange={(value) => { - console.log("value", value); value && setTradeState(value as TradeSide); }} > diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index 0584712a3a..f68f1c2e9b 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -127,11 +127,6 @@ export function useTradeSimulation({ setIsLoading({ isLoading: true, status: SimulationStatus.PREPARING }); - console.log({ - selectedBank, - selectedSecondaryBank, - }); - try { const loopingResult = await calculateLooping({ marginfiClient: marginfiClient, @@ -196,7 +191,6 @@ export function useTradeSimulation({ if (prevDebouncedAmount !== debouncedAmount || prevDebouncedLeverage !== debouncedLeverage) { // Only set to PREPARING if we're actually going to simulate if (debouncedAmount > 0 && debouncedLeverage > 0) { - console.log("fetching trade txns"); fetchTradeTxns(debouncedAmount, debouncedLeverage); } } diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 51e7e2de4b..b5d4a96f7a 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -200,28 +200,6 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { setMaxLeverage, }); - React.useEffect(() => { - console.log({ - selectedBank: selectedBank?.meta.tokenSymbol, - selectedSecondaryBank: selectedSecondaryBank?.meta.tokenSymbol, - }); - - let borrowBank, depositBank; - - if (tradeState === "long") { - depositBank = activePoolExtended.tokenBank; - borrowBank = activePoolExtended.quoteBank; - } else { - depositBank = activePoolExtended.quoteBank; - borrowBank = activePoolExtended.tokenBank; - } - - console.log({ - depositBank: depositBank?.meta.tokenSymbol, - borrowBank: borrowBank?.meta.tokenSymbol, - }); - }, [selectedBank, selectedSecondaryBank]); - const isActiveWithCollat = true; // TODO: figure out what this does? const actionMethods = React.useMemo( From cddee18ab06e343f0c6c3401e2dbe2aa3be1ad09 Mon Sep 17 00:00:00 2001 From: borcherd Date: Thu, 12 Dec 2024 11:00:40 +0100 Subject: [PATCH 09/27] feat: qa changes --- .../action-simuation-status.tsx | 76 +++++++++++++++++++ .../action-simulation-status/index.ts | 1 + .../common/trade-box-v2/components/index.ts | 1 + .../common/trade-box-v2/trade-box.tsx | 36 +++++---- 4 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/action-simuation-status.tsx create mode 100644 apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/index.ts diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/action-simuation-status.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/action-simuation-status.tsx new file mode 100644 index 0000000000..f253eab3e1 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/action-simuation-status.tsx @@ -0,0 +1,76 @@ +import React from "react"; + +import { IconCheck, IconX } from "@tabler/icons-react"; + +import { SimulationStatus } from "~/components/action-box-v2/utils"; // TODO +import { IconInfiniteLoader, IconLoader } from "~/components/ui/icons"; + +type ActionSimulationStatusProps = { + simulationStatus: SimulationStatus; + hasErrorMessages: boolean; + isActive: boolean; +}; + +enum SimulationCompleteStatus { + NULL = "NULL", + LOADING = "LOADING", + SUCCESS = "SUCCESS", + ERROR = "ERROR", +} + +const ActionSimulationStatus = ({ + simulationStatus, + hasErrorMessages = false, + isActive = false, +}: ActionSimulationStatusProps) => { + const [simulationCompleteStatus, setSimulationCompleteStatus] = React.useState( + SimulationCompleteStatus.NULL + ); + const [isNewSimulation, setIsNewSimulation] = React.useState(false); + + React.useEffect(() => { + if (simulationStatus === SimulationStatus.SIMULATING || simulationStatus === SimulationStatus.PREPARING) { + setSimulationCompleteStatus(SimulationCompleteStatus.LOADING); + setIsNewSimulation(false); + } else if (hasErrorMessages && !isNewSimulation) { + setSimulationCompleteStatus(SimulationCompleteStatus.ERROR); + } else if (simulationStatus === SimulationStatus.COMPLETE && !isNewSimulation) { + setSimulationCompleteStatus(SimulationCompleteStatus.SUCCESS); + } + }, [simulationStatus, hasErrorMessages, isNewSimulation]); + + React.useEffect(() => { + if (!isActive) { + setIsNewSimulation(true); + setSimulationCompleteStatus(SimulationCompleteStatus.NULL); + } + }, [isActive]); + + if (!isActive) { + return
; // Return empty div to align the settings button + } + + return ( +
+ {simulationCompleteStatus === SimulationCompleteStatus.LOADING && ( +

+ Simulating transaction... +

+ )} + + {simulationCompleteStatus === SimulationCompleteStatus.SUCCESS && ( +

+ Simulation complete! +

+ )} + + {simulationCompleteStatus === SimulationCompleteStatus.ERROR && ( +

+ Simulation failed +

+ )} +
+ ); +}; + +export { ActionSimulationStatus }; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/index.ts new file mode 100644 index 0000000000..67194a0ea3 --- /dev/null +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/index.ts @@ -0,0 +1 @@ +export * from "./action-simuation-status"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts index b05639b03c..5594104f19 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/index.ts @@ -7,3 +7,4 @@ export * from "./settings"; export * from "./amount-preview"; export * from "./action-button"; export * from "./leverage-slider"; +export * from "./action-simulation-status"; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index b5d4a96f7a..99990945ab 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -15,6 +15,7 @@ import { usePrevious, } from "@mrgnlabs/mrgn-utils"; import { IconSettings } from "@tabler/icons-react"; +import { ActionType, ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { ArenaPoolV2 } from "~/store/tradeStoreV2"; import { handleExecuteTradeAction, TradeSide } from "~/components/common/trade-box-v2/utils"; @@ -24,7 +25,7 @@ import { useWallet, useWalletStore } from "~/components/wallet-v2"; import { useExtendedPool } from "~/hooks/useExtendedPools"; import { useMarginfiClient } from "~/hooks/useMarginfiClient"; import { useWrappedAccount } from "~/hooks/useWrappedAccount"; -import { SimulationStatus } from "~/components/action-box-v2/utils"; +import { SimulationStatus } from "~/components/action-box-v2/utils"; // TODO import { useAmountDebounce } from "~/hooks/useAmountDebounce"; import { @@ -41,9 +42,7 @@ import { import { useTradeBoxStore } from "./store"; import { checkTradeActionAvailable } from "./utils"; import { useTradeSimulation, useActionAmounts } from "./hooks"; -import { ActionType, ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; -import { useActionContext } from "@mrgnlabs/mrgn-ui"; -import { UnderlineIcon } from "@radix-ui/react-icons"; +import { ActionSimulationStatus } from "./components"; interface TradeBoxV2Props { activePool: ArenaPoolV2; @@ -116,9 +115,9 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { groupPk: activePoolExtended.groupPk, banks: [activePoolExtended.tokenBank, activePoolExtended.quoteBank], }); - const { walletContextState, wallet, connected } = useWallet(); + const { wallet, connected } = useWallet(); const { connection } = useConnection(); - const { amount, debouncedAmount, walletAmount, maxAmount } = useActionAmounts({ + const { amount, debouncedAmount, maxAmount } = useActionAmounts({ amountRaw, activePool: activePoolExtended, collateralBank: selectedBank, @@ -176,12 +175,12 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { } }, [errorMessage]); - // TODO: on load, reset everything React.useEffect(() => { refreshState(); }, []); const { actionSummary, refreshSimulation } = useTradeSimulation({ + // TODO: do we need actionSummary debouncedAmount: debouncedAmount ?? 0, debouncedLeverage: debouncedLeverage ?? 0, selectedBank: selectedBank, @@ -274,6 +273,9 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { // TODO: update the messaging within the toast. Might need tailored functions in the sdk for trading console.log("error", error); const toast = error.multiStepToast as MultiStepToastHandle; // TODO: check if this works, not sure it does + if (!toast) { + return; + } const txs = error.actionTxns as ActionTxns; let retry = undefined; if (error.retry && toast && txs) { @@ -400,17 +402,23 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { buttonLabel={tradeState === "long" ? "Long" : "Short"} tradeState={tradeState} /> - setSlippageBps(value * 100)} - slippageBps={slippageBps / 100} - > -
+
+ 0} + isActive={selectedBank && amount > 0 ? true : false} + /> + setSlippageBps(value * 100)} + slippageBps={slippageBps / 100} + > -
- + +
+ Date: Fri, 13 Dec 2024 09:20:53 +0100 Subject: [PATCH 10/27] chores: QA fixes & todo implementations --- .../action-simuation-status.tsx | 4 +- .../components/amount-input/amount-input.tsx | 8 +- .../components/max-action/max-action.tsx | 2 +- .../components/stats/utils/stats-utils.tsx | 2 +- .../hooks/use-trade-simulation.ts | 21 +--- .../trade-box-v2/store/trade-box-store.tsx | 37 -------- .../common/trade-box-v2/trade-box.tsx | 95 ++++++++++++++----- .../trade-box-v2/utils/trade-box.consts.ts | 18 ++++ .../utils/trade-simulation.utils.ts | 2 +- 9 files changed, 99 insertions(+), 90 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/action-simuation-status.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/action-simuation-status.tsx index f253eab3e1..0be11f5601 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/action-simuation-status.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-simulation-status/action-simuation-status.tsx @@ -2,8 +2,8 @@ import React from "react"; import { IconCheck, IconX } from "@tabler/icons-react"; -import { SimulationStatus } from "~/components/action-box-v2/utils"; // TODO -import { IconInfiniteLoader, IconLoader } from "~/components/ui/icons"; +import { IconLoader } from "~/components/ui/icons"; +import { SimulationStatus } from "~/components/common/trade-box-v2/utils"; type ActionSimulationStatusProps = { simulationStatus: SimulationStatus; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx index 162cdad27f..688b388536 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx @@ -4,6 +4,7 @@ import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { Input } from "~/components/ui/input"; import { MaxAction } from "./components"; import { ArenaBank } from "~/store/tradeStoreV2"; +import { tokenPriceFormatter } from "@mrgnlabs/mrgn-common"; interface AmountInputProps { maxAmount: number; @@ -34,17 +35,10 @@ export const AmountInput = ({ ref={amountInputRef} inputMode="decimal" value={amount} - // disabled={isInputDisabled} // TODO: add this onChange={(e) => handleAmountChange(e.target.value)} placeholder="0" className="bg-transparent shadow-none min-w-[130px] h-auto py-0 pr-0 text-right outline-none focus-visible:outline-none focus-visible:ring-0 border-none text-base font-medium" /> - {/* {amount !== null && amount > 0 && selectedBank && ( - - {tokenPriceFormatter(amount * selectedBank.info.oraclePrice.priceRealtime.price.toNumber())} - - )} */}{" "} - {/* // TODO: add this usd price */}
diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx index c89f13e2ee..8d72743312 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx @@ -23,7 +23,7 @@ export const MaxAction = ({ maxAmount, collateralBank, setAmount }: TradeActionP amount: "-", showWalletIcon: false, }; - } // TODO: is this necessary since collateralBank is defined? + } const formatAmount = (maxAmount?: number, symbol?: string) => maxAmount !== undefined ? `${clampedNumeralFormatter(maxAmount)} ${symbol?.toUpperCase()}` : "-"; // TODO: use dynamicNumeralFormatter diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx index b951054acf..fe81404526 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx @@ -160,4 +160,4 @@ export function getSimulationStats(simulationResult: SimulationResult, extendedP healthFactor, liquidationPrice, }; -} // TODO: a lot of this code is copy pasted from old code, need to clean up +} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index f68f1c2e9b..96efdb9760 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -120,7 +120,7 @@ export function useTradeSimulation({ lastValidBlockHeight: undefined, actualDepositAmount: 0, borrowAmount: new BigNumber(0), - }); // TODO: create init state from this + }); setSimulationResult(null); return; } @@ -142,6 +142,10 @@ export function useTradeSimulation({ if (loopingResult && "actionQuote" in loopingResult) { setActionTxns(loopingResult); + handleSimulation([ + ...(actionTxns?.additionalTxns ?? []), + ...(actionTxns?.actionTxn ? [actionTxns?.actionTxn] : []), + ]); } else { const errorMessage = loopingResult ?? @@ -196,22 +200,7 @@ export function useTradeSimulation({ } }, [debouncedAmount, debouncedLeverage, fetchTradeTxns, prevDebouncedAmount, prevDebouncedLeverage]); - React.useEffect(() => { - // Only run simulation if we have transactions to simulate - if (actionTxns?.actionTxn || (actionTxns?.additionalTxns?.length ?? 0) > 0) { - handleSimulation([ - ...(actionTxns?.additionalTxns ?? []), - ...(actionTxns?.actionTxn ? [actionTxns?.actionTxn] : []), - ]); - } else { - // If no transactions, move back to idle state - setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionTxns]); - // Fetch max leverage based when the secondary bank changes - // Not needed rn i think but when we do pay with any token it will be needed React.useEffect(() => { if (!selectedSecondaryBank) { return; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx index 4abee5bf7d..0bcc084681 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx @@ -14,9 +14,6 @@ interface TradeBoxState { leverage: number; maxLeverage: number; - depositLstApy: number | null; - borrowLstApy: number | null; - selectedBank: ArenaBank | null; selectedSecondaryBank: ArenaBank | null; @@ -37,8 +34,6 @@ interface TradeBoxState { setSelectedBank: (bank: ArenaBank | null) => void; setSelectedSecondaryBank: (bank: ArenaBank | null) => void; setMaxLeverage: (maxLeverage: number) => void; - setDepositLstApy: (bank: ArenaBank) => void; - setBorrowLstApy: (bank: ArenaBank) => void; } const initialState = { @@ -50,8 +45,6 @@ const initialState = { selectedBank: null, selectedSecondaryBank: null, maxLeverage: 0, - depositLstApy: null, - borrowLstApy: null, actionTxns: { actionTxn: null, @@ -78,8 +71,6 @@ const stateCreator: StateCreator = (set, get) => ({ leverage: initialState.leverage, actionTxns: initialState.actionTxns, errorMessage: null, - depositLstApy: initialState.depositLstApy, // TODO: can we remove? Not using anywhere - borrowLstApy: initialState.borrowLstApy, // TODO: can we remove? Not using anywhere }); }, @@ -135,9 +126,6 @@ const stateCreator: StateCreator = (set, get) => ({ const hasBankChanged = !tokenBank || !selectedBank || !tokenBank.address.equals(selectedBank.address); if (hasBankChanged) { - if (tokenBank) { - get().setDepositLstApy(tokenBank); - } set({ selectedBank: tokenBank, amountRaw: initialState.amountRaw, @@ -154,9 +142,6 @@ const stateCreator: StateCreator = (set, get) => ({ !secondaryBank || !selectedSecondaryBank || !secondaryBank.address.equals(selectedSecondaryBank.address); if (hasBankChanged) { - if (secondaryBank) { - get().setBorrowLstApy(secondaryBank); - } set({ selectedSecondaryBank: secondaryBank, amountRaw: initialState.amountRaw, @@ -172,28 +157,6 @@ const stateCreator: StateCreator = (set, get) => ({ setMaxLeverage(maxLeverage) { set({ maxLeverage }); }, - - async setDepositLstApy(bank: ArenaBank) { - const lstsArr = Object.keys(LSTS_SOLANA_COMPASS_MAP); - if (!lstsArr.includes(bank.meta.tokenSymbol)) { - set({ depositLstApy: null }); - return; - } else { - const depositLstApy = await calculateLstYield(bank); - set({ depositLstApy }); - } - }, - - async setBorrowLstApy(bank: ArenaBank) { - const lstsArr = Object.keys(LSTS_SOLANA_COMPASS_MAP); - if (!lstsArr.includes(bank.meta.tokenSymbol)) { - set({ borrowLstApy: null }); - return; - } else { - const borrowLstApy = await calculateLstYield(bank); - set({ borrowLstApy }); - } - }, }); export { createTradeBoxStore }; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 99990945ab..402d503242 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -5,27 +5,26 @@ import React from "react"; import { ActionMessageType, ActionTxns, + capture, ExecuteLoopingActionProps, formatAmount, IndividualFlowError, + LoopActionTxns, MultiStepToastHandle, - PreviousTxn, showErrorToast, useConnection, - usePrevious, } from "@mrgnlabs/mrgn-utils"; import { IconSettings } from "@tabler/icons-react"; import { ActionType, ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { ArenaPoolV2 } from "~/store/tradeStoreV2"; -import { handleExecuteTradeAction, TradeSide } from "~/components/common/trade-box-v2/utils"; +import { handleExecuteTradeAction, SimulationStatus, TradeSide } from "~/components/common/trade-box-v2/utils"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { useTradeStoreV2, useUiStore } from "~/store"; import { useWallet, useWalletStore } from "~/components/wallet-v2"; import { useExtendedPool } from "~/hooks/useExtendedPools"; import { useMarginfiClient } from "~/hooks/useMarginfiClient"; import { useWrappedAccount } from "~/hooks/useWrappedAccount"; -import { SimulationStatus } from "~/components/action-box-v2/utils"; // TODO import { useAmountDebounce } from "~/hooks/useAmountDebounce"; import { @@ -43,6 +42,7 @@ import { useTradeBoxStore } from "./store"; import { checkTradeActionAvailable } from "./utils"; import { useTradeSimulation, useActionAmounts } from "./hooks"; import { ActionSimulationStatus } from "./components"; +import { PreviousTxn } from "~/types"; interface TradeBoxV2Props { activePool: ArenaPoolV2; @@ -92,12 +92,22 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { state.setSelectedSecondaryBank, state.setMaxLeverage, ]); - const [slippageBps, setSlippageBps, platformFeeBps, broadcastType, priorityFees] = useUiStore((state) => [ + const [ + slippageBps, + setSlippageBps, + platformFeeBps, + broadcastType, + priorityFees, + setIsActionComplete, + setPreviousTxn, + ] = useUiStore((state) => [ state.slippageBps, state.setSlippageBps, state.platformFeeBps, state.broadcastType, state.priorityFees, + state.setIsActionComplete, + state.setPreviousTxn, ]); const [setIsWalletOpen] = useWalletStore((state) => [state.setIsWalletOpen]); const [fetchTradeState, nativeSolBalance, setIsRefreshingStore, refreshGroup] = useTradeStoreV2((state) => [ @@ -179,8 +189,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { refreshState(); }, []); - const { actionSummary, refreshSimulation } = useTradeSimulation({ - // TODO: do we need actionSummary + const { refreshSimulation } = useTradeSimulation({ debouncedAmount: debouncedAmount ?? 0, debouncedLeverage: debouncedLeverage ?? 0, selectedBank: selectedBank, @@ -245,16 +254,21 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { callbacks.captureEvent && callbacks.captureEvent(event, properties); }, setIsComplete: (txnSigs) => { + const _actionTxns = params.actionTxns as LoopActionTxns; callbacks.setIsActionComplete(true); callbacks.setPreviousTxn({ + txnType: "TRADING", txn: txnSigs[txnSigs.length - 1] ?? "", - txnType: "LOOP", - loopOptions: { + tradingOptions: { depositBank: params.depositBank as ActiveBankInfo, borrowBank: params.borrowBank as ActiveBankInfo, + initDepositAmount: params.depositAmount.toString(), depositAmount: params.actualDepositAmount, borrowAmount: params.borrowAmount.toNumber(), leverage: leverage, + type: tradeState, + quote: _actionTxns.actionQuote!, + entryPrice: activePoolExtended.tokenBank.info.oraclePrice.priceRealtime.price.toNumber(), }, }); @@ -294,10 +308,22 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { const retryTradeAction = React.useCallback( (params: ExecuteLoopingActionProps, leverage: number) => { executeAction(params, leverage, { - captureEvent: () => {}, // TODO: implement this - setIsActionComplete: () => {}, // TODO: implement this - setPreviousTxn: () => {}, // TODO: implement this - onComplete: () => {}, // TODO: implement this + captureEvent: () => { + capture("trade_action_retry", { + group: activePoolExtended.groupPk.toBase58(), + bank: selectedBank?.meta.tokenSymbol, + }); + }, + setIsActionComplete: setIsActionComplete, + setPreviousTxn, + onComplete: () => { + refreshGroup({ + connection, + wallet, + groupPk: activePoolExtended.groupPk, + banks: [activePoolExtended.tokenBank.address, activePoolExtended.quoteBank.address], + }); + }, setIsLoading: setIsTransactionExecuting, setAmountRaw, retryCallback: (txns: ActionTxns, multiStepToast: MultiStepToastHandle) => { @@ -305,7 +331,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { }, }); }, - [setAmountRaw, setIsTransactionExecuting] + [setAmountRaw, setIsTransactionExecuting, setIsActionComplete, setPreviousTxn] ); const handleTradeAction = React.useCallback(async () => { @@ -333,17 +359,42 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { }; executeAction(params, leverage, { - captureEvent: () => {}, // TODO: implement this - setIsActionComplete: () => {}, // TODO: implement this - setPreviousTxn: () => {}, // TODO: implement this - onComplete: () => {}, // TODO: implement this + captureEvent: () => { + capture("trade_action_execute", { + group: activePoolExtended.groupPk.toBase58(), + bank: selectedBank?.meta.tokenSymbol, + }); + }, + setIsActionComplete: setIsActionComplete, + setPreviousTxn, + onComplete: () => { + refreshGroup({ + connection, + wallet, + groupPk: activePoolExtended.groupPk, + banks: [activePoolExtended.tokenBank.address, activePoolExtended.quoteBank.address], + }); + }, setIsLoading: setIsTransactionExecuting, setAmountRaw, retryCallback: (txns: ActionTxns, multiStepToast: MultiStepToastHandle) => { retryTradeAction({ ...params, actionTxns: txns, multiStepToast }, leverage); }, }); - }, []); + }, [ + client, + selectedBank, + selectedSecondaryBank, + actionTxns, + priorityFees, + broadcastType, + wrappedAccount, + amount, + leverage, + setIsActionComplete, + setIsTransactionExecuting, + setAmountRaw, + ]); return ( @@ -429,9 +480,3 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { ); }; - -/* -TODO: -- when wallet is connected but store is loading, show to user - -*/ diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.consts.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.consts.ts index 5b479ae6c0..408ff66b01 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.consts.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.consts.ts @@ -1 +1,19 @@ +import { Transaction, VersionedTransaction } from "@solana/web3.js"; + +import { MarginfiAccountWrapper } from "@mrgnlabs/marginfi-client-v2"; +import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; + export type TradeSide = "long" | "short"; + +export enum SimulationStatus { + IDLE = "idle", + PREPARING = "preparing", + SIMULATING = "simulating", + COMPLETE = "complete", +} + +export interface SimulateActionProps { + txns: (VersionedTransaction | Transaction)[]; + account: MarginfiAccountWrapper; + bank: ExtendedBankInfo; +} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts index 57c0dbafba..dcbceaed33 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts @@ -2,7 +2,6 @@ import { SimulationResult } from "@mrgnlabs/marginfi-client-v2"; import { ActionMessageType, handleSimulationError, LoopActionTxns } from "@mrgnlabs/mrgn-utils"; -import { SimulateActionProps } from "~/components/action-box-v2/actions/loop-box/utils"; // TODO: fix this import import { ArenaBank } from "~/store/tradeStoreV2"; import { ActionPreview, @@ -14,6 +13,7 @@ import { import { SimulatedActionPreview } from "~/components/action-box-v2/actions/lend-box/utils"; import { nativeToUi } from "@mrgnlabs/mrgn-common"; import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; +import { SimulateActionProps } from "./trade-box.consts"; export const getSimulationResult = async (props: SimulateActionProps) => { let actionMethod: ActionMessageType | undefined = undefined; From db024ec43a373fd1cc11328f9fc694feb81ff1c2 Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 09:51:15 +0100 Subject: [PATCH 11/27] feat: TODO's & QA fixes --- .../components/amount-input/amount-input.tsx | 2 -- .../components/max-action/max-action.tsx | 11 +++++------ .../amount-preview/amount-preview.tsx | 17 +++-------------- .../trade-box-v2/hooks/use-trade-simulation.ts | 18 ++++++++++++++---- .../common/trade-box-v2/trade-box.tsx | 14 ++++++++------ .../mrgn-common/src/utils/formatters.utils.ts | 2 +- 6 files changed, 31 insertions(+), 33 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx index 688b388536..9596d0094f 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx @@ -1,10 +1,8 @@ import React from "react"; -import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { Input } from "~/components/ui/input"; import { MaxAction } from "./components"; import { ArenaBank } from "~/store/tradeStoreV2"; -import { tokenPriceFormatter } from "@mrgnlabs/mrgn-common"; interface AmountInputProps { maxAmount: number; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx index 8d72743312..52b5ec35f1 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/components/max-action/max-action.tsx @@ -1,5 +1,4 @@ -import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; -import { clampedNumeralFormatter } from "@mrgnlabs/mrgn-common"; +import { dynamicNumeralFormatter } from "@mrgnlabs/mrgn-common"; import React from "react"; import { ArenaBank } from "~/store/tradeStoreV2"; @@ -11,8 +10,6 @@ interface TradeActionProps { } export const MaxAction = ({ maxAmount, collateralBank, setAmount }: TradeActionProps) => { - const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); // TODO: remove this - const maxLabel = React.useMemo((): { amount: string; showWalletIcon?: boolean; @@ -26,7 +23,7 @@ export const MaxAction = ({ maxAmount, collateralBank, setAmount }: TradeActionP } const formatAmount = (maxAmount?: number, symbol?: string) => - maxAmount !== undefined ? `${clampedNumeralFormatter(maxAmount)} ${symbol?.toUpperCase()}` : "-"; // TODO: use dynamicNumeralFormatter + maxAmount !== undefined ? `${dynamicNumeralFormatter(maxAmount)} ${symbol?.toUpperCase()}` : "-"; return { amount: formatAmount(maxAmount, collateralBank.meta.tokenSymbol), @@ -45,7 +42,9 @@ export const MaxAction = ({ maxAmount, collateralBank, setAmount }: TradeActionP diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx index 655dc2ad5a..1d1717aea5 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { numeralFormatter } from "@mrgnlabs/mrgn-common"; +import { dynamicNumeralFormatter } from "@mrgnlabs/mrgn-common"; import { cn } from "@mrgnlabs/mrgn-utils"; import { IconLoader } from "~/components/ui/icons"; @@ -9,7 +9,7 @@ import { ArenaBank } from "~/store/tradeStoreV2"; interface AmountPreviewProps { tradeSide: "long" | "short"; selectedBank: ArenaBank | null; - amount?: number; + amount: number; isLoading?: boolean; } @@ -18,18 +18,7 @@ export const AmountPreview = ({ tradeSide, amount, isLoading, selectedBank }: Am
- {isLoading ? ( - - ) : amount ? ( - amount < 0.01 && amount > 0 ? ( - "< 0.01" - ) : ( - numeralFormatter(amount) - ) - ) : ( - "-" - )}{" "} - {/* TODO: get rid of the amount check above and use dynamicnumeralformatter */} + {isLoading ? : dynamicNumeralFormatter(amount)}{" "} {selectedBank?.meta.tokenSymbol.toUpperCase()}
diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index 96efdb9760..e9b3934f0e 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -142,10 +142,6 @@ export function useTradeSimulation({ if (loopingResult && "actionQuote" in loopingResult) { setActionTxns(loopingResult); - handleSimulation([ - ...(actionTxns?.additionalTxns ?? []), - ...(actionTxns?.actionTxn ? [actionTxns?.actionTxn] : []), - ]); } else { const errorMessage = loopingResult ?? @@ -200,6 +196,20 @@ export function useTradeSimulation({ } }, [debouncedAmount, debouncedLeverage, fetchTradeTxns, prevDebouncedAmount, prevDebouncedLeverage]); + React.useEffect(() => { + // Only run simulation if we have transactions to simulate + if (actionTxns?.actionTxn || (actionTxns?.additionalTxns?.length ?? 0) > 0) { + handleSimulation([ + ...(actionTxns?.additionalTxns ?? []), + ...(actionTxns?.actionTxn ? [actionTxns?.actionTxn] : []), + ]); + } else { + // If no transactions, move back to idle state + setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionTxns]); + // Fetch max leverage based when the secondary bank changes React.useEffect(() => { if (!selectedSecondaryBank) { diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 402d503242..0b028f0f30 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -418,12 +418,14 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { maxLeverage={maxLeverage} setLeverageAmount={setLeverage} /> - + {leveragedAmount > 0 && ( + + )} {actionMethods && actionMethods.concat(additionalActionMessages).some((method) => method.description) && ( = 0.01) { + if (Math.abs(value) >= minDisplay) { return numeral(value).format("0,0.[0000]a"); } From ee0da2f0ece77a71895cddfb343be999b931e7e0 Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 10:07:57 +0100 Subject: [PATCH 12/27] chore: styling changes --- .../components/amount-input/amount-input.tsx | 15 ++++++++++++--- .../components/amount-preview/amount-preview.tsx | 2 +- .../trade-box-v2/components/header/header.tsx | 12 ++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx index 9596d0094f..55f145a583 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-input/amount-input.tsx @@ -1,5 +1,5 @@ import React from "react"; - +import Image from "next/image"; import { Input } from "~/components/ui/input"; import { MaxAction } from "./components"; import { ArenaBank } from "~/store/tradeStoreV2"; @@ -22,9 +22,18 @@ export const AmountInput = ({ const amountInputRef = React.useRef(null); return ( -
+
- + + {collateralBank?.meta.tokenLogoUri && ( + {collateralBank?.meta.tokenSymbol} + )} {collateralBank?.meta.tokenSymbol.toUpperCase()}
diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx index 1d1717aea5..df4f3922db 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/amount-preview/amount-preview.tsx @@ -16,7 +16,7 @@ interface AmountPreviewProps { export const AmountPreview = ({ tradeSide, amount, isLoading, selectedBank }: AmountPreviewProps) => { return (
-
+
{isLoading ? : dynamicNumeralFormatter(amount)}{" "} {selectedBank?.meta.tokenSymbol.toUpperCase()} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx index 20518d21fa..718b7011ef 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx @@ -21,12 +21,12 @@ export const Header = ({ activePool }: HeaderProps) => { router.push(`/trade/${pool.groupPk.toBase58()}`); }} > -
+
{activePool.tokenBank.meta.tokenSymbol}

@@ -34,14 +34,14 @@ export const Header = ({ activePool }: HeaderProps) => {

-
+
Entry price - $122.00 + $122.00
24h volume - $1.65m + $1.65m
From 6749a6c480d3177953200ff7115ea976b2eda87a Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 10:12:59 +0100 Subject: [PATCH 13/27] chore: remove logs --- .../trade-box-v2/components/info-messages/info-messages.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx index f7c0ce1c2a..e98d1b2be7 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx @@ -186,8 +186,6 @@ export const InfoMessages = ({ ); const renderContent = () => { - console.log("actionMethods", actionMethods); - console.log("additionalChecks", additionalChecks); if (!connected) return null; switch (true) { @@ -198,7 +196,6 @@ export const InfoMessages = ({ return renderShortWarning(); case isActiveWithCollat: - console.log("isActiveWithCollat", isActiveWithCollat); return renderActionMethodMessages(); default: From aea04736942c8ac9da370c20b644557cc4d966cd Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 10:33:06 +0100 Subject: [PATCH 14/27] feat: taillored function to looping & updated formatted --- .../common/trade-box-v2/trade-box.tsx | 11 +-- .../trade-box-v2/utils/trade-action.utils.ts | 14 +-- .../mrgn-common/src/utils/formatters.utils.ts | 5 +- packages/mrgn-utils/src/actions/actions.ts | 22 +++++ .../mrgn-utils/src/actions/individualFlows.ts | 86 +++++++++++++++++++ 5 files changed, 121 insertions(+), 17 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 0b028f0f30..5534e275c7 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -6,7 +6,7 @@ import { ActionMessageType, ActionTxns, capture, - ExecuteLoopingActionProps, + ExecuteTradeActionProps, formatAmount, IndividualFlowError, LoopActionTxns, @@ -235,7 +235,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { // Trading Actions // ///////////////////// const executeAction = async ( - params: ExecuteLoopingActionProps, + params: ExecuteTradeActionProps, leverage: number, callbacks: { captureEvent?: (event: string, properties?: Record) => void; @@ -247,7 +247,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { retryCallback: (txs: ActionTxns, toast: MultiStepToastHandle) => void; } ) => { - const action = async (params: ExecuteLoopingActionProps) => { + const action = async (params: ExecuteTradeActionProps) => { await handleExecuteTradeAction({ props: params, captureEvent: (event, properties) => { @@ -306,7 +306,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { }; const retryTradeAction = React.useCallback( - (params: ExecuteLoopingActionProps, leverage: number) => { + (params: ExecuteTradeActionProps, leverage: number) => { executeAction(params, leverage, { captureEvent: () => { capture("trade_action_retry", { @@ -339,7 +339,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { return; } - const params: ExecuteLoopingActionProps = { + const params: ExecuteTradeActionProps = { marginfiClient: client, actionTxns, processOpts: { @@ -356,6 +356,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { borrowBank: selectedSecondaryBank, quote: actionTxns.actionQuote!, connection: client.provider.connection, + tradeSide: tradeState, }; executeAction(params, leverage, { diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts index 093e6e39aa..321ffcfd24 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts @@ -1,18 +1,10 @@ import { v4 as uuidv4 } from "uuid"; -import { - ActionMessageType, - calculateLoopingParams, - CalculateLoopingProps, - executeLoopingAction, - LoopActionTxns, - ExecuteLoopingActionProps, - IndividualFlowError, -} from "@mrgnlabs/mrgn-utils"; +import { IndividualFlowError, executeTradeAction, ExecuteTradeActionProps } from "@mrgnlabs/mrgn-utils"; import { ExecuteActionsCallbackProps } from "~/components/action-box-v2/types"; interface ExecuteTradeActionsProps extends ExecuteActionsCallbackProps { - props: ExecuteLoopingActionProps; + props: ExecuteTradeActionProps; } export const handleExecuteTradeAction = async ({ @@ -33,7 +25,7 @@ export const handleExecuteTradeAction = async ({ priorityFee: props.processOpts?.priorityFeeMicro ?? 0, }); - const txnSig = await executeLoopingAction(props); + const txnSig = await executeTradeAction(props); setIsLoading(false); diff --git a/packages/mrgn-common/src/utils/formatters.utils.ts b/packages/mrgn-common/src/utils/formatters.utils.ts index 49db14a019..26e38b7d4e 100644 --- a/packages/mrgn-common/src/utils/formatters.utils.ts +++ b/packages/mrgn-common/src/utils/formatters.utils.ts @@ -36,6 +36,8 @@ interface dynamicNumeralFormatterOptions { export const dynamicNumeralFormatter = (value: number, options: dynamicNumeralFormatterOptions = {}) => { const { minDisplay = 0.00001, tokenPrice } = options; + console.log("value", value); + if (value === 0) return "0"; if (Math.abs(value) < minDisplay) { @@ -47,7 +49,8 @@ export const dynamicNumeralFormatter = (value: number, options: dynamicNumeralFo } if (Math.abs(value) >= minDisplay) { - return numeral(value).format("0,0.[0000]a"); + const decimalPlaces = Math.max(0, Math.ceil(-Math.log10(minDisplay))); + return numeral(value).format(`0,0.[${"0".repeat(decimalPlaces)}]`); } if (tokenPrice) { diff --git a/packages/mrgn-utils/src/actions/actions.ts b/packages/mrgn-utils/src/actions/actions.ts index a136ef47cc..6f98280f3a 100644 --- a/packages/mrgn-utils/src/actions/actions.ts +++ b/packages/mrgn-utils/src/actions/actions.ts @@ -13,6 +13,7 @@ import { borrow, withdraw, looping, + trade, repayWithCollat, createAccountAndDeposit, createAccount, @@ -125,6 +126,27 @@ export async function executeLoopingAction(params: ExecuteLoopingActionProps) { return txnSig; } +export interface ExecuteTradeActionProps extends LoopingProps { + marginfiClient: MarginfiClient; + actionTxns: ActionTxns; + processOpts: ProcessTransactionsClientOpts; + txOpts: TransactionOptions; + tradeSide: "long" | "short"; +} + +export async function executeTradeAction(params: ExecuteTradeActionProps) { + let txnSig: string[] | undefined; + + if (!params.marginfiAccount) { + showErrorToast("Marginfi account not ready."); + return; + } + + txnSig = await trade(params); + + return txnSig; +} + export async function executeLstAction({ actionMode, marginfiClient, diff --git a/packages/mrgn-utils/src/actions/individualFlows.ts b/packages/mrgn-utils/src/actions/individualFlows.ts index 1540f5ec56..9f2a4512c0 100644 --- a/packages/mrgn-utils/src/actions/individualFlows.ts +++ b/packages/mrgn-utils/src/actions/individualFlows.ts @@ -587,6 +587,92 @@ export async function looping({ } } +interface TradeFnProps extends LoopingProps { + marginfiClient: MarginfiClient; + actionTxns: ActionTxns; + processOpts: ProcessTransactionsClientOpts; + txOpts: TransactionOptions; + tradeSide: "long" | "short"; +} + +export async function trade({ + marginfiClient, + actionTxns, + processOpts, + txOpts, + multiStepToast, + ...tradingProps +}: TradeFnProps) { + if (marginfiClient === null) { + showErrorToast({ message: "Marginfi client not ready" }); + return; + } + + if (!multiStepToast) { + const steps = getSteps(actionTxns); + + multiStepToast = new MultiStepToastHandle("Trading", [ + ...steps, + { + label: `${tradingProps.tradeSide === "long" ? "Longing" : "Shorting"} ${dynamicNumeralFormatter( + tradingProps.depositAmount + )} ${tradingProps.depositBank.meta.tokenSymbol} with ${dynamicNumeralFormatter( + tradingProps.borrowAmount.toNumber() + )} ${tradingProps.borrowBank.meta.tokenSymbol}`, + }, + ]); + multiStepToast.start(); + } else { + multiStepToast.resetAndStart(); + } + + try { + let sigs: string[] = []; + + if (actionTxns?.actionTxn) { + sigs = await marginfiClient.processTransactions( + [...actionTxns.additionalTxns, actionTxns.actionTxn], + { + ...processOpts, + callback: (index, success, sig, stepsToAdvance) => + success && + multiStepToast.setSuccessAndNext(stepsToAdvance, sig, composeExplorerUrl(sig, processOpts?.broadcastType)), + }, + txOpts + ); + } else { + // TODO fix flashloan builder to use processOpts + const { flashloanTx, additionalTxs } = await loopingBuilder({ + ...tradingProps, + }); + sigs = await marginfiClient.processTransactions([...additionalTxs, flashloanTx], processOpts, txOpts); + } + + multiStepToast.setSuccess( + sigs[sigs.length - 1], + composeExplorerUrl(sigs[sigs.length - 1], processOpts?.broadcastType) + ); + return sigs; + } catch (error: any) { + console.log(`Error while looping`); + console.log(error); + if (!(error instanceof ProcessTransactionError || error instanceof SolanaJSONRPCError)) { + captureSentryException(error, JSON.stringify(error), { + action: "looping", + wallet: tradingProps.marginfiAccount?.authority?.toBase58(), + bank: tradingProps.borrowBank.meta.tokenSymbol, + amount: tradingProps.borrowAmount.toString(), + }); + } + + handleIndividualFlowError({ + error, + actionTxns, + multiStepToast, + }); + } +} + interface RepayWithCollatFnProps extends RepayWithCollatProps { marginfiClient: MarginfiClient; actionTxns: ActionTxns; From 2e45be16bb1861e001787e9dfc57186bcd369ac1 Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 10:36:20 +0100 Subject: [PATCH 15/27] chore: remove log --- packages/mrgn-common/src/utils/formatters.utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/mrgn-common/src/utils/formatters.utils.ts b/packages/mrgn-common/src/utils/formatters.utils.ts index 26e38b7d4e..4cf0784878 100644 --- a/packages/mrgn-common/src/utils/formatters.utils.ts +++ b/packages/mrgn-common/src/utils/formatters.utils.ts @@ -36,8 +36,6 @@ interface dynamicNumeralFormatterOptions { export const dynamicNumeralFormatter = (value: number, options: dynamicNumeralFormatterOptions = {}) => { const { minDisplay = 0.00001, tokenPrice } = options; - console.log("value", value); - if (value === 0) return "0"; if (Math.abs(value) < minDisplay) { From e9430682e33859c0a99d1f92df87a1640bad9dc3 Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 10:36:32 +0100 Subject: [PATCH 16/27] chore: use provided tradeside --- .../src/components/common/trade-box-v2/trade-box.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 5534e275c7..0805766725 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -26,6 +26,7 @@ import { useExtendedPool } from "~/hooks/useExtendedPools"; import { useMarginfiClient } from "~/hooks/useMarginfiClient"; import { useWrappedAccount } from "~/hooks/useWrappedAccount"; import { useAmountDebounce } from "~/hooks/useAmountDebounce"; +import { PreviousTxn } from "~/types"; import { ActionButton, @@ -37,12 +38,11 @@ import { Stats, TradingBoxSettingsDialog, InfoMessages, + ActionSimulationStatus, } from "./components"; import { useTradeBoxStore } from "./store"; import { checkTradeActionAvailable } from "./utils"; import { useTradeSimulation, useActionAmounts } from "./hooks"; -import { ActionSimulationStatus } from "./components"; -import { PreviousTxn } from "~/types"; interface TradeBoxV2Props { activePool: ArenaPoolV2; @@ -189,6 +189,10 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { refreshState(); }, []); + React.useEffect(() => { + setTradeState(side); + }, [side]); + const { refreshSimulation } = useTradeSimulation({ debouncedAmount: debouncedAmount ?? 0, debouncedLeverage: debouncedLeverage ?? 0, From 890f6715385101275790b7cb363dc9cbbb593026 Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 12:52:59 +0100 Subject: [PATCH 17/27] feat: updated onComplete and added short-long restriction check --- .../hooks/use-trade-simulation.ts | 6 +- .../common/trade-box-v2/trade-box.tsx | 56 +++++++++++-------- .../trade-box-v2/utils/trade-box.utils.ts | 20 +++++++ 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index e9b3934f0e..777ba1a0e4 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -32,6 +32,7 @@ export type TradeSimulationProps = { simulationResult: SimulationResult | null; wrappedAccount: MarginfiAccountWrapper | null; accountSummary?: AccountSummary; + isEnabled: boolean; slippageBps: number; platformFeeBps: number; @@ -55,7 +56,7 @@ export function useTradeSimulation({ slippageBps, platformFeeBps, accountSummary, - + isEnabled, setActionTxns, setErrorMessage, setIsLoading, @@ -188,13 +189,14 @@ export function useTradeSimulation({ }, [selectedBank, selectedSecondaryBank, setErrorMessage, setMaxLeverage]); React.useEffect(() => { + // console.log("isEnabled", isEnabled); if (prevDebouncedAmount !== debouncedAmount || prevDebouncedLeverage !== debouncedLeverage) { // Only set to PREPARING if we're actually going to simulate if (debouncedAmount > 0 && debouncedLeverage > 0) { fetchTradeTxns(debouncedAmount, debouncedLeverage); } } - }, [debouncedAmount, debouncedLeverage, fetchTradeTxns, prevDebouncedAmount, prevDebouncedLeverage]); + }, [debouncedAmount, debouncedLeverage, fetchTradeTxns, prevDebouncedAmount, prevDebouncedLeverage, isEnabled]); React.useEffect(() => { // Only run simulation if we have transactions to simulate diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 0805766725..9f0fa2b3b0 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -153,7 +153,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { ); // Memos - const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); // The fuck is this lol? + const numberFormater = React.useMemo(() => new Intl.NumberFormat("en-US", { maximumFractionDigits: 10 }), []); const leveragedAmount = React.useMemo(() => { if (tradeState === "long") { @@ -163,6 +163,20 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { } }, [tradeState, actionTxns]); + const actionMethods = React.useMemo( + () => + checkTradeActionAvailable({ + amount, + connected, + collateralBank: selectedBank, + secondaryBank: selectedSecondaryBank, + actionQuote: actionTxns.actionQuote, + tradeState, + }), + + [amount, connected, activePoolExtended, actionTxns, tradeState, selectedSecondaryBank, selectedBank] + ); + // Effects React.useEffect(() => { if (activePoolExtended) { @@ -205,6 +219,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { actionTxns: actionTxns, simulationResult: null, accountSummary: accountSummary ?? undefined, + isEnabled: !actionMethods.concat(additionalActionMessages).filter((value) => value.isEnabled === false).length, setActionTxns: setActionTxns, setErrorMessage: setErrorMessage, setIsLoading: setIsSimulating, @@ -212,20 +227,13 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { setMaxLeverage, }); - const isActiveWithCollat = true; // TODO: figure out what this does? - - const actionMethods = React.useMemo( - () => - checkTradeActionAvailable({ - amount, - connected, - collateralBank: selectedBank, - secondaryBank: selectedSecondaryBank, - actionQuote: actionTxns.actionQuote, - }), + React.useEffect(() => { + console.log("actionMethods", actionMethods); + console.log("additionalActionMessages", additionalActionMessages); + console.log(!actionMethods.concat(additionalActionMessages).filter((value) => value.isEnabled === false).length); + }, [actionMethods]); - [amount, connected, activePoolExtended, actionTxns, tradeState] - ); + const isActiveWithCollat = true; // TODO: figure out what this does? const handleAmountChange = React.useCallback( (amountRaw: string) => { @@ -279,18 +287,22 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { callbacks.onComplete && callbacks.onComplete({ txn: txnSigs[txnSigs.length - 1] ?? "", - txnType: "LEND", - lendingOptions: { - amount: params.depositAmount, - type: ActionType.Loop, - bank: params.depositBank as ActiveBankInfo, + txnType: "TRADING", + tradingOptions: { + depositBank: params.depositBank as ActiveBankInfo, + borrowBank: params.borrowBank as ActiveBankInfo, + initDepositAmount: params.depositAmount.toString(), + depositAmount: params.actualDepositAmount, + borrowAmount: params.borrowAmount.toNumber(), + leverage: leverage, + type: tradeState, + quote: _actionTxns.actionQuote!, + entryPrice: activePoolExtended.tokenBank.info.oraclePrice.priceRealtime.price.toNumber(), }, }); }, setError: (error: IndividualFlowError) => { - // TODO: update the messaging within the toast. Might need tailored functions in the sdk for trading - console.log("error", error); - const toast = error.multiStepToast as MultiStepToastHandle; // TODO: check if this works, not sure it does + const toast = error.multiStepToast as MultiStepToastHandle; if (!toast) { return; } diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts index 518af41a65..bf5d4573c8 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts @@ -1,5 +1,6 @@ import { QuoteResponse } from "@jup-ag/api"; import { OperationalState } from "@mrgnlabs/marginfi-client-v2"; +import { ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { ActionMessageType, DYNAMIC_SIMULATION_ERRORS, isBankOracleStale } from "@mrgnlabs/mrgn-utils"; import { ArenaBank } from "~/store/tradeStoreV2"; @@ -9,6 +10,7 @@ interface CheckTradeActionAvailableProps { collateralBank: ArenaBank | null; secondaryBank: ArenaBank | null; actionQuote: QuoteResponse | null; + tradeState: "long" | "short"; } export function checkTradeActionAvailable({ @@ -17,6 +19,7 @@ export function checkTradeActionAvailable({ collateralBank, secondaryBank, actionQuote, + tradeState, }: CheckTradeActionAvailableProps): ActionMessageType[] { let checks: ActionMessageType[] = []; @@ -26,6 +29,9 @@ export function checkTradeActionAvailable({ const generalChecks = getGeneralChecks(amount ?? 0); if (generalChecks) checks.push(...generalChecks); + const tradeSpecificChecks = getTradeSpecificChecks(tradeState, secondaryBank); + if (tradeSpecificChecks) checks.push(...tradeSpecificChecks); + // allert checks if (collateralBank) { const tradeChecks = canBeTraded(collateralBank, secondaryBank, actionQuote); @@ -99,6 +105,20 @@ function canBeTraded( if ((repayBankInfo && isBankOracleStale(repayBankInfo)) || (targetBankInfo && isBankOracleStale(targetBankInfo))) { checks.push(DYNAMIC_SIMULATION_ERRORS.STALE_CHECK("Trading")); } + return checks; +} + +function getTradeSpecificChecks(tradeState: "long" | "short", secondaryBank: ArenaBank | null): ActionMessageType[] { + let checks: ActionMessageType[] = []; + + if (secondaryBank?.isActive && (secondaryBank as ActiveBankInfo)?.position.isLending) { + checks.push({ + isEnabled: false, + description: `You cannot ${tradeState} while you have an active ${ + tradeState === "long" ? "short" : "long" + } position for this token.`, + }); + } return checks; } From d7ed08b32f1955b73945ffabd092ba8cd5c0b726 Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 15:04:14 +0100 Subject: [PATCH 18/27] feat: tx fetching & simulation refactor & qa changes --- .../trade-box-v2/hooks/use-action-amounts.ts | 2 +- .../hooks/use-trade-simulation.ts | 254 +++++++++++------- .../common/trade-box-v2/trade-box.tsx | 60 +++-- .../trade-box-v2/utils/trade-box.utils.ts | 17 +- 4 files changed, 200 insertions(+), 133 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-action-amounts.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-action-amounts.ts index 2cd140821c..ceed58ba33 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-action-amounts.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-action-amounts.ts @@ -38,7 +38,7 @@ export function useActionAmounts({ } return collateralBank.userInfo.maxDeposit; - }, [collateralBank, activePool]); + }, [collateralBank]); return { amount, diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index 777ba1a0e4..927fbcc67a 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -8,6 +8,7 @@ import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; import { SolanaTransaction } from "@mrgnlabs/mrgn-common"; import { ActionMessageType, + CalculateLoopingProps, DYNAMIC_SIMULATION_ERRORS, extractErrorString, LoopActionTxns, @@ -67,69 +68,109 @@ export function useTradeSimulation({ const prevDebouncedLeverage = usePrevious(debouncedLeverage); const prevSelectedSecondaryBank = usePrevious(selectedSecondaryBank); - const handleSimulation = React.useCallback( - async (txns: (VersionedTransaction | Transaction)[]) => { - try { - setIsLoading({ isLoading: true, status: SimulationStatus.SIMULATING }); - if (wrappedAccount && selectedBank && txns.length > 0) { - const simulationResult = await getSimulationResult({ - account: wrappedAccount, - bank: selectedBank, - txns, - }); - if (simulationResult.actionMethod) { - setErrorMessage(simulationResult.actionMethod); - throw new Error(simulationResult.actionMethod.description); - } else { - setErrorMessage(null); - setSimulationResult(simulationResult.simulationResult); - } - } else { - throw new Error("account, bank or transactions are null"); - } - } catch (error) { - console.error("Error simulating transaction", error); - setSimulationResult(null); - } finally { - setIsLoading({ isLoading: false, status: SimulationStatus.COMPLETE }); - } - }, - [selectedBank, wrappedAccount, setErrorMessage, setIsLoading, setSimulationResult] - ); + const handleError = ( + actionMessage: ActionMessageType | string, + callbacks: { + setErrorMessage: (error: ActionMessageType | null) => void; + setSimulationResult: (result: SimulationResult | null) => void; + setActionTxns: (actionTxns: LoopActionTxns) => void; + setIsLoading: ({ isLoading, status }: { isLoading: boolean; status: SimulationStatus }) => void; + } + ) => { + if (typeof actionMessage === "string") { + const errorMessage = extractErrorString(actionMessage); + const _actionMessage: ActionMessageType = { + isEnabled: true, + description: errorMessage, + }; + callbacks.setErrorMessage(_actionMessage); + } else { + callbacks.setErrorMessage(actionMessage); + } + callbacks.setSimulationResult(null); + callbacks.setActionTxns({ + actionTxn: null, + additionalTxns: [], + actionQuote: null, + lastValidBlockHeight: undefined, + actualDepositAmount: 0, + borrowAmount: new BigNumber(0), + }); + console.error( + "Error simulating transaction", + typeof actionMessage === "string" ? extractErrorString(actionMessage) : actionMessage.description + ); + callbacks.setIsLoading({ isLoading: false, status: SimulationStatus.COMPLETE }); + }; - const handleActionSummary = React.useCallback( - (summary?: AccountSummary, result?: SimulationResult) => { - if (wrappedAccount && summary && selectedBank && actionTxns) { - return calculateSummary({ - simulationResult: result ?? undefined, - bank: selectedBank, - accountSummary: summary, - actionTxns: actionTxns, - }); - } - }, - [selectedBank, wrappedAccount, actionTxns] - ); + const simulationAction = async (props: { + account: MarginfiAccountWrapper; + bank: ArenaBank; + txns: (VersionedTransaction | Transaction)[]; + }): Promise<{ + simulationResult: SimulationResult | null; + actionMessage: ActionMessageType | null; + }> => { + if (props.txns.length > 0) { + const simulationResult = await getSimulationResult(props); - const fetchTradeTxns = React.useCallback( - async (amount: number, leverage: number) => { - if (amount === 0 || leverage === 0 || !selectedBank || !selectedSecondaryBank || !marginfiClient) { - setActionTxns({ - actionTxn: null, - additionalTxns: [], - actionQuote: null, - lastValidBlockHeight: undefined, - actualDepositAmount: 0, - borrowAmount: new BigNumber(0), - }); - setSimulationResult(null); - return; + if (simulationResult.actionMethod) { + return { simulationResult: null, actionMessage: simulationResult.actionMethod }; + } else if (simulationResult.simulationResult) { + return { simulationResult: simulationResult.simulationResult, actionMessage: null }; + } else { + const errorMessage = DYNAMIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED_CHECK(props.bank.meta.tokenSymbol); // TODO: update + return { simulationResult: null, actionMessage: errorMessage }; } + } else { + throw new Error("account, bank or transactions are null"); + } + }; - setIsLoading({ isLoading: true, status: SimulationStatus.PREPARING }); + const fetchTradeTxnsAction = async ( + props: CalculateLoopingProps + ): Promise<{ actionTxns: LoopActionTxns | null; actionMessage: ActionMessageType | null }> => { + try { + const loopingResult = await calculateLooping(props); + + if (loopingResult && "actionQuote" in loopingResult) { + return { actionTxns: loopingResult, actionMessage: null }; + } else { + const errorMessage = + loopingResult ?? DYNAMIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED_CHECK(props.borrowBank.meta.tokenSymbol); + // TODO: update + return { actionTxns: null, actionMessage: errorMessage }; + } + } catch (error) { + return { actionTxns: null, actionMessage: STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED }; // TODO: update + } + }; + const handleSimulation = React.useCallback( + async (amount: number, leverage: number) => { try { - const loopingResult = await calculateLooping({ + if ( + amount === 0 || + leverage === 0 || + !selectedBank || + !selectedSecondaryBank || + !marginfiClient || + !wrappedAccount + ) { + setActionTxns({ + actionTxn: null, + additionalTxns: [], + actionQuote: null, + lastValidBlockHeight: undefined, + actualDepositAmount: 0, + borrowAmount: new BigNumber(0), + }); + setSimulationResult(null); + return; + } + setIsLoading({ isLoading: true, status: SimulationStatus.SIMULATING }); + + const loopActionTxns = await fetchTradeTxnsAction({ marginfiClient: marginfiClient, marginfiAccount: wrappedAccount, depositBank: selectedBank, @@ -141,22 +182,54 @@ export function useTradeSimulation({ platformFeeBps: platformFeeBps, }); - if (loopingResult && "actionQuote" in loopingResult) { - setActionTxns(loopingResult); + if (loopActionTxns.actionMessage || loopActionTxns.actionTxns === null) { + handleError(loopActionTxns.actionMessage ?? STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED, { + // TODO: update error message + setErrorMessage, + setSimulationResult, + setActionTxns, + setIsLoading, + }); + return; + } + + const simulationResult = await simulationAction({ + account: wrappedAccount, + bank: selectedBank, + txns: [ + ...(loopActionTxns?.actionTxns?.additionalTxns ?? []), + ...(loopActionTxns?.actionTxns?.actionTxn ? [loopActionTxns?.actionTxns?.actionTxn] : []), + ], + }); + + if (simulationResult.actionMessage || simulationResult.simulationResult === null) { + handleError(simulationResult.actionMessage ?? STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED, { + // TODO: update + setErrorMessage, + setSimulationResult, + setActionTxns, + setIsLoading, + }); + return; + } else if (simulationResult.simulationResult) { + setSimulationResult(simulationResult.simulationResult); + setActionTxns(loopActionTxns.actionTxns); } else { - const errorMessage = - loopingResult ?? - DYNAMIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED_CHECK(selectedSecondaryBank.meta.tokenSymbol); - // TODO: update - - setErrorMessage(errorMessage); - console.error("Error building looping transaction: ", errorMessage.description); - setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); + throw new Error("Unknown error"); } } catch (error) { - console.error("Error building looping transaction:", error); - setErrorMessage(STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED); // TODO: update - setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); + console.error("Error simulating transaction", error); + setSimulationResult(null); + setActionTxns({ + actionTxn: null, + additionalTxns: [], + actionQuote: null, + lastValidBlockHeight: undefined, + actualDepositAmount: 0, + borrowAmount: new BigNumber(0), + }); + } finally { + setIsLoading({ isLoading: false, status: SimulationStatus.COMPLETE }); } }, [ @@ -164,12 +237,12 @@ export function useTradeSimulation({ selectedSecondaryBank, marginfiClient, wrappedAccount, + setIsLoading, slippageBps, platformFeeBps, - setErrorMessage, - setIsLoading, setActionTxns, setSimulationResult, + setErrorMessage, ] ); @@ -189,47 +262,24 @@ export function useTradeSimulation({ }, [selectedBank, selectedSecondaryBank, setErrorMessage, setMaxLeverage]); React.useEffect(() => { - // console.log("isEnabled", isEnabled); - if (prevDebouncedAmount !== debouncedAmount || prevDebouncedLeverage !== debouncedLeverage) { + if ((prevDebouncedAmount !== debouncedAmount || prevDebouncedLeverage !== debouncedLeverage) && isEnabled) { // Only set to PREPARING if we're actually going to simulate if (debouncedAmount > 0 && debouncedLeverage > 0) { - fetchTradeTxns(debouncedAmount, debouncedLeverage); + handleSimulation(debouncedAmount, debouncedLeverage); } } - }, [debouncedAmount, debouncedLeverage, fetchTradeTxns, prevDebouncedAmount, prevDebouncedLeverage, isEnabled]); - - React.useEffect(() => { - // Only run simulation if we have transactions to simulate - if (actionTxns?.actionTxn || (actionTxns?.additionalTxns?.length ?? 0) > 0) { - handleSimulation([ - ...(actionTxns?.additionalTxns ?? []), - ...(actionTxns?.actionTxn ? [actionTxns?.actionTxn] : []), - ]); - } else { - // If no transactions, move back to idle state - setIsLoading({ isLoading: false, status: SimulationStatus.IDLE }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionTxns]); + }, [debouncedAmount, debouncedLeverage, handleSimulation, isEnabled, prevDebouncedAmount, prevDebouncedLeverage]); // Fetch max leverage based when the secondary bank changes React.useEffect(() => { - if (!selectedSecondaryBank) { - return; - } - const hasBankChanged = !prevSelectedSecondaryBank?.address.equals(selectedSecondaryBank.address); - if (hasBankChanged) { + if (selectedSecondaryBank && prevSelectedSecondaryBank?.address !== selectedSecondaryBank.address) { fetchMaxLeverage(); } }, [selectedSecondaryBank, prevSelectedSecondaryBank, fetchMaxLeverage]); - const actionSummary = React.useMemo(() => { - return handleActionSummary(accountSummary, simulationResult ?? undefined); - }, [accountSummary, simulationResult, handleActionSummary]); - const refreshSimulation = React.useCallback(async () => { - await fetchTradeTxns(debouncedAmount ?? 0, debouncedLeverage ?? 0); - }, [fetchTradeTxns, debouncedAmount, debouncedLeverage]); + await handleSimulation(debouncedAmount ?? 0, debouncedLeverage ?? 0); + }, [handleSimulation, debouncedAmount, debouncedLeverage]); - return { actionSummary, refreshSimulation }; + return { refreshSimulation }; } diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index 9f0fa2b3b0..967b8b1f21 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -17,7 +17,7 @@ import { import { IconSettings } from "@tabler/icons-react"; import { ActionType, ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; -import { ArenaPoolV2 } from "~/store/tradeStoreV2"; +import { ArenaPoolV2, ArenaPoolV2Extended } from "~/store/tradeStoreV2"; import { handleExecuteTradeAction, SimulationStatus, TradeSide } from "~/components/common/trade-box-v2/utils"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { useTradeStoreV2, useUiStore } from "~/store"; @@ -174,9 +174,15 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { tradeState, }), - [amount, connected, activePoolExtended, actionTxns, tradeState, selectedSecondaryBank, selectedBank] + [amount, connected, actionTxns, tradeState, selectedSecondaryBank, selectedBank] ); + const isDisabled = React.useMemo(() => { + if (!actionTxns?.actionQuote) return true; + if (actionMethods.concat(additionalActionMessages).filter((value) => value.isEnabled === false).length) return true; + return false; + }, [actionMethods, additionalActionMessages, actionTxns]); + // Effects React.useEffect(() => { if (activePoolExtended) { @@ -188,11 +194,13 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { setSelectedSecondaryBank(activePoolExtended.quoteBank); } } - }, [activePoolExtended, tradeState]); + }, [activePoolExtended, setSelectedBank, setSelectedSecondaryBank, tradeState]); React.useEffect(() => { if (errorMessage && errorMessage.description) { - showErrorToast(errorMessage?.description); + if (errorMessage.actionMethod === "ERROR") { + showErrorToast(errorMessage?.description); + } setAdditionalActionMessages([errorMessage]); } else { setAdditionalActionMessages([]); @@ -201,11 +209,11 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { React.useEffect(() => { refreshState(); - }, []); + }, [refreshState]); React.useEffect(() => { setTradeState(side); - }, [side]); + }, [setTradeState, side]); const { refreshSimulation } = useTradeSimulation({ debouncedAmount: debouncedAmount ?? 0, @@ -227,12 +235,6 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { setMaxLeverage, }); - React.useEffect(() => { - console.log("actionMethods", actionMethods); - console.log("additionalActionMessages", additionalActionMessages); - console.log(!actionMethods.concat(additionalActionMessages).filter((value) => value.isEnabled === false).length); - }, [actionMethods]); - const isActiveWithCollat = true; // TODO: figure out what this does? const handleAmountChange = React.useCallback( @@ -240,7 +242,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { const amount = formatAmount(amountRaw, maxAmount, selectedBank ?? null, numberFormater); setAmountRaw(amount); }, - [maxAmount, selectedBank, numberFormater] + [maxAmount, selectedBank, numberFormater, setAmountRaw] ); ///////////////////// @@ -249,6 +251,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { const executeAction = async ( params: ExecuteTradeActionProps, leverage: number, + activePoolExtended: ArenaPoolV2Extended, callbacks: { captureEvent?: (event: string, properties?: Record) => void; setIsActionComplete: (isComplete: boolean) => void; @@ -278,7 +281,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { depositAmount: params.actualDepositAmount, borrowAmount: params.borrowAmount.toNumber(), leverage: leverage, - type: tradeState, + type: params.tradeSide, quote: _actionTxns.actionQuote!, entryPrice: activePoolExtended.tokenBank.info.oraclePrice.priceRealtime.price.toNumber(), }, @@ -295,7 +298,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { depositAmount: params.actualDepositAmount, borrowAmount: params.borrowAmount.toNumber(), leverage: leverage, - type: tradeState, + type: params.tradeSide, quote: _actionTxns.actionQuote!, entryPrice: activePoolExtended.tokenBank.info.oraclePrice.priceRealtime.price.toNumber(), }, @@ -323,7 +326,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { const retryTradeAction = React.useCallback( (params: ExecuteTradeActionProps, leverage: number) => { - executeAction(params, leverage, { + executeAction(params, leverage, activePoolExtended, { captureEvent: () => { capture("trade_action_retry", { group: activePoolExtended.groupPk.toBase58(), @@ -347,7 +350,16 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { }, }); }, - [setAmountRaw, setIsTransactionExecuting, setIsActionComplete, setPreviousTxn] + [ + activePoolExtended, + setIsActionComplete, + setPreviousTxn, + setAmountRaw, + selectedBank?.meta.tokenSymbol, + refreshGroup, + connection, + wallet, + ] ); const handleTradeAction = React.useCallback(async () => { @@ -375,7 +387,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { tradeSide: tradeState, }; - executeAction(params, leverage, { + executeAction(params, leverage, activePoolExtended, { captureEvent: () => { capture("trade_action_execute", { group: activePoolExtended.groupPk.toBase58(), @@ -407,10 +419,16 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { broadcastType, wrappedAccount, amount, + tradeState, leverage, setIsActionComplete, - setIsTransactionExecuting, + setPreviousTxn, setAmountRaw, + refreshGroup, + connection, + wallet, + retryTradeAction, + activePoolExtended, ]); return ( @@ -462,9 +480,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { value.isEnabled === false).length - } + isEnabled={!isDisabled} connected={connected} handleAction={() => { handleTradeAction(); diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts index bf5d4573c8..959d280629 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-box.utils.ts @@ -87,13 +87,7 @@ function canBeTraded( ); } - if (!swapQuote) { - checks.push({ - isEnabled: false, - }); - } - - if (swapQuote?.priceImpactPct && Number(swapQuote.priceImpactPct) > 0.01) { + if (swapQuote && swapQuote?.priceImpactPct && Number(swapQuote.priceImpactPct) > 0.01) { //invert if (swapQuote?.priceImpactPct && Number(swapQuote.priceImpactPct) > 0.05) { checks.push(DYNAMIC_SIMULATION_ERRORS.PRICE_IMPACT_ERROR_CHECK(Number(swapQuote.priceImpactPct))); @@ -102,7 +96,14 @@ function canBeTraded( } } - if ((repayBankInfo && isBankOracleStale(repayBankInfo)) || (targetBankInfo && isBankOracleStale(targetBankInfo))) { + if ( + (repayBankInfo && + repayBankInfo?.info.rawBank.config.oracleSetup !== "SwitchboardV2" && + isBankOracleStale(repayBankInfo)) || + (targetBankInfo && + targetBankInfo.info.rawBank.config.oracleSetup !== "SwitchboardV2" && + isBankOracleStale(targetBankInfo)) + ) { checks.push(DYNAMIC_SIMULATION_ERRORS.STALE_CHECK("Trading")); } return checks; From 4318c1611584087df297d4ca9dacb134fa9a3e46 Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 17:03:40 +0100 Subject: [PATCH 19/27] feat: update stats --- .../trade-box-v2/components/stats/stats.tsx | 13 ++- .../components/stats/utils/stats-utils.tsx | 104 ++++++------------ 2 files changed, 47 insertions(+), 70 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx index 9d52267df1..92dc250594 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx @@ -28,7 +28,18 @@ export const Stats = ({ activePool, accountSummary, simulationResult, actionTxns {stats && (
{stats.map((stat, idx) => ( - + ))} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx index fe81404526..e06d57cc00 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx @@ -25,50 +25,41 @@ export function generateTradeStats(props: generateTradeStatsProps) { value: () => <>{tokenPriceFormatter(props.extendedPool.tokenBank.info.state.price)}, }); - // simulation stat - if (props.simulationResult) { - const simStats = getSimulationStats(props.simulationResult, props.extendedPool); - const currentLiquidationPrice = - props.extendedPool.tokenBank.isActive && - props.extendedPool.tokenBank.position.liquidationPrice && - props.extendedPool.tokenBank.position.liquidationPrice > 0.01 - ? usdFormatter.format(props.extendedPool.tokenBank.position.liquidationPrice) - : null; - const simulatedLiqPrice = simStats?.liquidationPrice ? usdFormatter.format(simStats?.liquidationPrice) : null; - const showLiqComparison = currentLiquidationPrice && simulatedLiqPrice; - stats.push({ - label: "Liquidation price", - value: () => ( - <> - {currentLiquidationPrice && {currentLiquidationPrice}} - {showLiqComparison && } - {simulatedLiqPrice && {simulatedLiqPrice}} - - ), - }); - } - - // platform fee stat - const platformFeeBps = props.actionTxns?.actionQuote?.platformFee - ? Number(props.actionTxns.actionQuote.platformFee?.feeBps) - : undefined; - if (platformFeeBps) { - stats.push({ - label: "Platform fee", - value: () => <>{percentFormatter.format(platformFeeBps / 10000)}, - }); - } - - // price impact stat - const priceImpactPct = props.actionTxns?.actionQuote - ? Number(props.actionTxns.actionQuote.priceImpactPct) - : undefined; - if (priceImpactPct) { - stats.push({ - label: "Price impact", - color: priceImpactPct > 0.05 ? "DESTRUCTIVE" : priceImpactPct > 0.01 ? "ALERT" : "SUCCESS", - value: () => <>{percentFormatter.format(priceImpactPct)}, - }); + if (props.actionTxns) { + // slippage stat + const slippageBps = props.actionTxns?.actionQuote?.slippageBps; + if (slippageBps) { + stats.push({ + label: "Slippage", + color: slippageBps > 500 ? "ALERT" : "SUCCESS", + value: () => <>{percentFormatter.format(slippageBps / 10000)}, + }); + } + + // platform fee stat + const platformFeeBps = props.actionTxns?.actionQuote?.platformFee + ? Number(props.actionTxns.actionQuote.platformFee?.feeBps) + : undefined; + + if (platformFeeBps) { + stats.push({ + label: "Platform fee", + value: () => <>{percentFormatter.format(platformFeeBps / 10000)}, + }); + } + + // price impact stat + const priceImpactPct = props.actionTxns?.actionQuote + ? Number(props.actionTxns.actionQuote.priceImpactPct) + : undefined; + + if (priceImpactPct !== undefined) { + stats.push({ + label: "Price impact", + color: priceImpactPct > 0.05 ? "DESTRUCTIVE" : priceImpactPct > 0.01 ? "ALERT" : "SUCCESS", + value: () => <>{percentFormatter.format(priceImpactPct)}, + }); + } } // oracle stat @@ -94,31 +85,6 @@ export function generateTradeStats(props: generateTradeStatsProps) { ), }); - const accountSummary = props.accountSummary; - if (accountSummary) { - // total deposits stat - stats.push({ - label: "Total deposits", - value: () => ( - <> - {props.extendedPool.tokenBank.info.state.totalDeposits.toFixed(2)}{" "} - {props.extendedPool.tokenBank.meta.tokenSymbol} - - ), - }); - - // total borrows stat - stats.push({ - label: "Total borrows", - value: () => ( - <> - {props.extendedPool.tokenBank.info.state.totalBorrows.toFixed(2)}{" "} - {props.extendedPool.tokenBank.meta.tokenSymbol} - - ), - }); - } - return stats; } From a90ca74758d009c8e41100382c433731edb8315b Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 17:19:36 +0100 Subject: [PATCH 20/27] chore: add url to oracle img --- .../components/stats/utils/stats-utils.tsx | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx index e06d57cc00..714ff4cfdb 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx @@ -2,7 +2,7 @@ import { MarginRequirementType, SimulationResult } from "@mrgnlabs/marginfi-clie import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; import { percentFormatter, tokenPriceFormatter, usdFormatter } from "@mrgnlabs/mrgn-common"; import { LoopActionTxns } from "@mrgnlabs/mrgn-utils"; -import { IconArrowRight } from "@tabler/icons-react"; +import Link from "next/link"; import { PreviewStat } from "~/components/action-box-v2/utils"; import { IconPyth } from "~/components/ui/icons"; import { IconSwitchboard } from "~/components/ui/icons"; @@ -63,24 +63,44 @@ export function generateTradeStats(props: generateTradeStatsProps) { } // oracle stat - let oracle = ""; + let oracle = { + name: "", + link: "", + }; + switch (props.extendedPool.tokenBank.info.rawBank.config.oracleSetup) { case "PythLegacy": - oracle = "Pyth"; + oracle = { + name: "Pyth", + link: "https://pyth.network/", + }; break; case "PythPushOracle": - oracle = "Pyth"; + oracle = { + name: "Pyth", + link: "https://pyth.network/", + }; break; case "SwitchboardV2": - oracle = "Switchboard"; + oracle = { + name: "Switchboard", + link: `https://ondemand.switchboard.xyz/solana/mainnet/feed/${props.extendedPool.tokenBank.info.rawBank.config.oracleKeys[0].toBase58()}`, + }; + break; + case "SwitchboardPull": + oracle = { + name: "Switchboard", + link: `https://ondemand.switchboard.xyz/solana/mainnet/feed/${props.extendedPool.tokenBank.info.rawBank.config.oracleKeys[0].toBase58()}`, + }; break; } stats.push({ label: "Oracle", value: () => ( <> - {oracle} - {oracle === "Pyth" ? : } + + {oracle.name === "Pyth" ? : } + ), }); From 5eaed239b30e09349393116a2ceaffec4148d626 Mon Sep 17 00:00:00 2001 From: Kobe Date: Fri, 13 Dec 2024 17:25:25 +0100 Subject: [PATCH 21/27] feat: better handeling for new accounts & seperated account tx --- .../hooks/use-trade-simulation.ts | 29 ++++--- .../trade-box-v2/utils/trade-action.utils.ts | 75 ++++++++++++++++++- .../utils/trade-simulation.utils.ts | 1 + .../marginfi-client-v2/src/clients/client.ts | 30 ++++++-- .../src/models/account/wrapper.ts | 1 + packages/mrgn-utils/src/actions/actions.ts | 16 ++-- .../src/actions/flashloans/builders.ts | 4 +- .../mrgn-utils/src/actions/individualFlows.ts | 8 +- packages/mrgn-utils/src/actions/types.ts | 2 + 9 files changed, 137 insertions(+), 29 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index 927fbcc67a..4e5363e0b1 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -20,7 +20,7 @@ import React from "react"; import { calculateLooping } from "~/components/action-box-v2/actions/loop-box/utils/loop-action.utils"; import { SimulationStatus } from "~/components/action-box-v2/utils"; import { ArenaBank, ArenaPoolV2Extended } from "~/store/tradeStoreV2"; -import { calculateSummary, getSimulationResult } from "../utils"; +import { calculateSummary, generateTradeTx, getSimulationResult } from "../utils"; import BigNumber from "bignumber.js"; export type TradeSimulationProps = { @@ -131,7 +131,10 @@ export function useTradeSimulation({ props: CalculateLoopingProps ): Promise<{ actionTxns: LoopActionTxns | null; actionMessage: ActionMessageType | null }> => { try { - const loopingResult = await calculateLooping(props); + const loopingResult = await generateTradeTx({ + ...props, + authority: props.marginfiAccount?.authority ?? props.marginfiClient.provider.publicKey, + }); if (loopingResult && "actionQuote" in loopingResult) { return { actionTxns: loopingResult, actionMessage: null }; @@ -149,14 +152,7 @@ export function useTradeSimulation({ const handleSimulation = React.useCallback( async (amount: number, leverage: number) => { try { - if ( - amount === 0 || - leverage === 0 || - !selectedBank || - !selectedSecondaryBank || - !marginfiClient || - !wrappedAccount - ) { + if (amount === 0 || leverage === 0 || !selectedBank || !selectedSecondaryBank || !marginfiClient) { setActionTxns({ actionTxn: null, additionalTxns: [], @@ -193,6 +189,16 @@ export function useTradeSimulation({ return; } + if (!loopActionTxns.actionTxns.accountCreationTx) { + setActionTxns(loopActionTxns.actionTxns); + return; + } + + if (!wrappedAccount) { + // throw error + return; + } + const simulationResult = await simulationAction({ account: wrappedAccount, bank: selectedBank, @@ -236,8 +242,9 @@ export function useTradeSimulation({ selectedBank, selectedSecondaryBank, marginfiClient, - wrappedAccount, setIsLoading, + fetchTradeTxnsAction, + wrappedAccount, slippageBps, platformFeeBps, setActionTxns, diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts index 321ffcfd24..407afaab47 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts @@ -1,7 +1,26 @@ import { v4 as uuidv4 } from "uuid"; -import { IndividualFlowError, executeTradeAction, ExecuteTradeActionProps } from "@mrgnlabs/mrgn-utils"; +import { + IndividualFlowError, + executeTradeAction, + ExecuteTradeActionProps, + CalculateLoopingProps, + LoopActionTxns, + ActionMessageType, + calculateLoopingParams, +} from "@mrgnlabs/mrgn-utils"; import { ExecuteActionsCallbackProps } from "~/components/action-box-v2/types"; +import { Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { + BalanceRaw, + MarginfiAccount, + MarginfiAccountRaw, + MarginfiAccountWrapper, + MarginfiClient, +} from "@mrgnlabs/marginfi-client-v2"; +import BN from "bn.js"; +import { bigNumberToWrappedI80F48, SolanaTransaction, WrappedI80F48 } from "@mrgnlabs/mrgn-common"; +import BigNumber from "bignumber.js"; interface ExecuteTradeActionsProps extends ExecuteActionsCallbackProps { props: ExecuteTradeActionProps; @@ -44,3 +63,57 @@ export const handleExecuteTradeAction = async ({ setError(error as IndividualFlowError); } }; + +interface GenerateTradeTxProps extends CalculateLoopingProps { + authority: PublicKey; +} + +export async function generateTradeTx(props: GenerateTradeTxProps): Promise { + const hasMarginfiAccount = !!props.marginfiAccount; + let accountCreationTx: SolanaTransaction | undefined; + + let finalAccount: MarginfiAccountWrapper | null = props.marginfiAccount; + + if (!hasMarginfiAccount) { + // if no marginfi account, we need to create one + console.log("Creating marginfi account"); + const marginfiAccountKeypair = Keypair.generate(); + + const dummyWrappedI80F48 = bigNumberToWrappedI80F48(new BigNumber(0)); + + const dummyBalances: BalanceRaw[] = Array(15).fill({ + active: false, + bankPk: new PublicKey("11111111111111111111111111111111"), + assetShares: dummyWrappedI80F48, + liabilityShares: dummyWrappedI80F48, + emissionsOutstanding: dummyWrappedI80F48, + lastUpdate: new BN(0), + }); + + const rawAccount: MarginfiAccountRaw = { + group: props.marginfiClient.group.address, + authority: props.authority, + lendingAccount: { balances: dummyBalances }, + accountFlags: new BN([0, 0, 0]), + }; + + const account = new MarginfiAccount(marginfiAccountKeypair.publicKey, rawAccount); + + const wrappedAccount = new MarginfiAccountWrapper(marginfiAccountKeypair.publicKey, props.marginfiClient, account); + + finalAccount = wrappedAccount; + + accountCreationTx = await props.marginfiClient.createMarginfiAccountTx({ accountKeypair: marginfiAccountKeypair }); + } + const result = await calculateLoopingParams({ ...props, marginfiAccount: finalAccount }); + + if (result && "actionQuote" in result) { + return { + ...result, + additionalTxns: [...(result.additionalTxns ?? [])], + accountCreationTx: accountCreationTx ?? undefined, + }; + } + + return result; +} diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts index dcbceaed33..70867fd9e2 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts @@ -23,6 +23,7 @@ export const getSimulationResult = async (props: SimulateActionProps) => { simulationResult = await simulateFlashLoan(props); } catch (error: any) { const actionString = "Looping"; + console.log("error", error); actionMethod = handleSimulationError(error, props.bank, false, actionString); } diff --git a/packages/marginfi-client-v2/src/clients/client.ts b/packages/marginfi-client-v2/src/clients/client.ts index a8276ccec4..5689abb35e 100644 --- a/packages/marginfi-client-v2/src/clients/client.ts +++ b/packages/marginfi-client-v2/src/clients/client.ts @@ -631,13 +631,7 @@ class MarginfiClient { const accountKeypair = Keypair.generate(); const newAccountKey = createOpts?.newAccountKey ?? accountKeypair.publicKey; - const ixs = await this.makeCreateMarginfiAccountIx(newAccountKey); - const signers = [...ixs.keys]; - // If there was no newAccountKey provided, we need to sign with the ephemeraKeypair we generated. - if (!createOpts?.newAccountKey) signers.push(accountKeypair); - - const tx = new Transaction().add(...ixs.instructions); - const solanaTx = addTransactionMetadata(tx, { signers, addressLookupTables: this.addressLookupTables }); + const solanaTx = await this.createMarginfiAccountTx({ accountKeypair }); const sig = await this.processTransaction(solanaTx, processOpts, txOpts); dbg("Created Marginfi account %s", sig); @@ -647,6 +641,28 @@ class MarginfiClient { : MarginfiAccountWrapper.fetch(newAccountKey, this, txOpts?.commitment); } + /** + * Create a transaction to initialize a new marginfi account under the authority of the user. + * + * @param createOpts - Options for creating the account + * @param createOpts.newAccountKey - Optional public key to use for the new account. If not provided, a new keypair will be generated. + * @returns Transaction that can be used to create a new marginfi account + */ + async createMarginfiAccountTx(createOpts?: { accountKeypair?: Keypair }): Promise { + const accountKeypair = createOpts?.accountKeypair ?? Keypair.generate(); + + const ixs = await this.makeCreateMarginfiAccountIx(accountKeypair.publicKey); + const signers = [...ixs.keys]; + // If there was no newAccountKey provided, we need to sign with the ephemeraKeypair we generated. + signers.push(accountKeypair); + + const tx = new Transaction().add(...ixs.instructions); + console.log({ tx }); + const solanaTx = addTransactionMetadata(tx, { signers, addressLookupTables: this.addressLookupTables }); + + return solanaTx; + } + /** * Create transaction instruction to initialize a new group. * diff --git a/packages/marginfi-client-v2/src/models/account/wrapper.ts b/packages/marginfi-client-v2/src/models/account/wrapper.ts index 4d287a949a..9fc114e191 100644 --- a/packages/marginfi-client-v2/src/models/account/wrapper.ts +++ b/packages/marginfi-client-v2/src/models/account/wrapper.ts @@ -870,6 +870,7 @@ class MarginfiAccountWrapper { txs: (VersionedTransaction | Transaction)[], banksToInspect: PublicKey[] ): Promise { + console.log("txs", txs); const [mfiAccountData, ...bankData] = await this.client.simulateTransactions(txs, [ this.address, ...banksToInspect, diff --git a/packages/mrgn-utils/src/actions/actions.ts b/packages/mrgn-utils/src/actions/actions.ts index 6f98280f3a..9fde010669 100644 --- a/packages/mrgn-utils/src/actions/actions.ts +++ b/packages/mrgn-utils/src/actions/actions.ts @@ -5,7 +5,14 @@ import { FEE_MARGIN, ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; import { TransactionOptions, WSOL_MINT } from "@mrgnlabs/mrgn-common"; import { MultiStepToastHandle, showErrorToast } from "../toasts"; -import { MarginfiActionParams, LstActionParams, ActionTxns, RepayWithCollatProps, LoopingProps } from "./types"; +import { + MarginfiActionParams, + LstActionParams, + ActionTxns, + RepayWithCollatProps, + LoopingProps, + LoopActionTxns, +} from "./types"; import { WalletContextStateOverride } from "../wallet"; import { deposit, @@ -128,7 +135,7 @@ export async function executeLoopingAction(params: ExecuteLoopingActionProps) { export interface ExecuteTradeActionProps extends LoopingProps { marginfiClient: MarginfiClient; - actionTxns: ActionTxns; + actionTxns: LoopActionTxns; processOpts: ProcessTransactionsClientOpts; txOpts: TransactionOptions; tradeSide: "long" | "short"; @@ -137,11 +144,6 @@ export interface ExecuteTradeActionProps extends LoopingProps { export async function executeTradeAction(params: ExecuteTradeActionProps) { let txnSig: string[] | undefined; - if (!params.marginfiAccount) { - showErrorToast("Marginfi account not ready."); - return; - } - txnSig = await trade(params); return txnSig; diff --git a/packages/mrgn-utils/src/actions/flashloans/builders.ts b/packages/mrgn-utils/src/actions/flashloans/builders.ts index 3357109a52..1bf0643422 100644 --- a/packages/mrgn-utils/src/actions/flashloans/builders.ts +++ b/packages/mrgn-utils/src/actions/flashloans/builders.ts @@ -244,7 +244,7 @@ export async function calculateLoopingParams({ let firstQuote; for (const maxAccounts of maxAccountsArr) { - const quoteParams = { + const quoteParams: QuoteGetRequest = { amount: borrowAmountNative, inputMint: loopingProps.borrowBank.info.state.mint.toBase58(), // borrow outputMint: loopingProps.depositBank.info.state.mint.toBase58(), // deposit @@ -252,7 +252,7 @@ export async function calculateLoopingParams({ platformFeeBps: platformFeeBps, // platform fee maxAccounts: maxAccounts, swapMode: "ExactIn", - } as QuoteGetRequest; + }; try { const swapQuote = await getSwapQuoteWithRetry(quoteParams); diff --git a/packages/mrgn-utils/src/actions/individualFlows.ts b/packages/mrgn-utils/src/actions/individualFlows.ts index 9f2a4512c0..8306812c5c 100644 --- a/packages/mrgn-utils/src/actions/individualFlows.ts +++ b/packages/mrgn-utils/src/actions/individualFlows.ts @@ -42,6 +42,7 @@ import { ActionTxns, RepayWithCollatProps, IndividualFlowError, + LoopActionTxns, } from "./types"; import { captureSentryException } from "../sentry.utils"; import { loopingBuilder, repayWithCollatBuilder } from "./flashloans"; @@ -589,7 +590,7 @@ export async function looping({ interface TradeFnProps extends LoopingProps { marginfiClient: MarginfiClient; - actionTxns: ActionTxns; + actionTxns: LoopActionTxns; processOpts: ProcessTransactionsClientOpts; txOpts: TransactionOptions; tradeSide: "long" | "short"; @@ -608,6 +609,11 @@ export async function trade({ return; } + if (actionTxns?.accountCreationTx) { + const accountCreationTx = actionTxns.accountCreationTx; + //process seperatetly then execute next + } + if (!multiStepToast) { const steps = getSteps(actionTxns); diff --git a/packages/mrgn-utils/src/actions/types.ts b/packages/mrgn-utils/src/actions/types.ts index 222185ab02..ce972927b8 100644 --- a/packages/mrgn-utils/src/actions/types.ts +++ b/packages/mrgn-utils/src/actions/types.ts @@ -74,6 +74,8 @@ export interface LoopActionTxns extends ActionTxns { lastValidBlockHeight?: number; actualDepositAmount: number; borrowAmount: BigNumber; + accountCreationTx?: SolanaTransaction; + marginfiAccount?: MarginfiAccountWrapper; } export interface ClosePositionActionTxns extends ActionTxns { From 4ccbf663dcd7b1192e4005967f9a4eb369613390 Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 17:36:30 +0100 Subject: [PATCH 22/27] chore: styling changes --- .../action-toggle/action-toggle.tsx | 2 +- .../trade-box-v2/components/header/header.tsx | 27 ++++++++++++------- .../trade-box-v2/components/stats/stats.tsx | 2 +- .../common/trade-box-v2/trade-box.tsx | 21 ++++++++------- .../mrgn-common/src/utils/formatters.utils.ts | 5 ++-- 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx index 291af247ae..e4295bee84 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/action-toggle/action-toggle.tsx @@ -10,7 +10,7 @@ export const ActionToggle = ({ tradeState, setTradeState }: ActionToggleProps) = return ( { value && setTradeState(value as TradeSide); diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx index 718b7011ef..5769a2974d 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/header/header.tsx @@ -1,3 +1,4 @@ +import { dynamicNumeralFormatter } from "@mrgnlabs/mrgn-common"; import { IconChevronDown } from "@tabler/icons-react"; import Image from "next/image"; import Link from "next/link"; @@ -8,25 +9,27 @@ import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; interface HeaderProps { activePool: ArenaPoolV2Extended; + entryPrice: number; + volume: number | undefined; } -export const Header = ({ activePool }: HeaderProps) => { +export const Header = ({ activePool, entryPrice, volume }: HeaderProps) => { const router = useRouter(); return ( -
+
{ router.push(`/trade/${pool.groupPk.toBase58()}`); }} > -
+
{activePool.tokenBank.meta.tokenSymbol}

@@ -37,13 +40,17 @@ export const Header = ({ activePool }: HeaderProps) => {
Entry price - $122.00 -
-
- 24h volume - $1.65m + ${dynamicNumeralFormatter(entryPrice, { maxDisplay: 100 })}
+ {volume && ( +
+ 24h volume + ${dynamicNumeralFormatter(volume, { maxDisplay: 10000 })} +
+ )}

); }; + +// TODO: add entry price and volume diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx index 92dc250594..657a74f904 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx @@ -26,7 +26,7 @@ export const Stats = ({ activePool, accountSummary, simulationResult, actionTxns return ( <> {stats && ( -
+
{stats.map((stat, idx) => ( { return ( -
+
- +
{
+
- - ); diff --git a/packages/mrgn-common/src/utils/formatters.utils.ts b/packages/mrgn-common/src/utils/formatters.utils.ts index 4cf0784878..9d6f239ac1 100644 --- a/packages/mrgn-common/src/utils/formatters.utils.ts +++ b/packages/mrgn-common/src/utils/formatters.utils.ts @@ -31,10 +31,11 @@ const numeralFormatter = (value: number) => { interface dynamicNumeralFormatterOptions { minDisplay?: number; tokenPrice?: number; + maxDisplay?: number; } export const dynamicNumeralFormatter = (value: number, options: dynamicNumeralFormatterOptions = {}) => { - const { minDisplay = 0.00001, tokenPrice } = options; + const { minDisplay = 0.00001, maxDisplay = 10000, tokenPrice } = options; if (value === 0) return "0"; @@ -42,7 +43,7 @@ export const dynamicNumeralFormatter = (value: number, options: dynamicNumeralFo return `<${minDisplay}`; } - if (Math.abs(value) > 10000) { + if (Math.abs(value) > maxDisplay) { return numeral(value).format("0,0.[00]a"); } From 89679e074da973a7ef413bdd236cf18f441048ad Mon Sep 17 00:00:00 2001 From: borcherd Date: Fri, 13 Dec 2024 19:33:41 +0100 Subject: [PATCH 23/27] feat: tx account creation work **WIP** --- .../trade-box-v2/components/stats/stats.tsx | 4 +- .../components/stats/utils/stats-utils.tsx | 4 +- .../hooks/use-trade-simulation.ts | 32 ++++++----- .../trade-box-v2/store/trade-box-store.tsx | 9 ++-- .../common/trade-box-v2/trade-box.tsx | 12 ++--- .../trade-box-v2/utils/trade-action.utils.ts | 6 +-- .../utils/trade-simulation.utils.ts | 6 +-- packages/mrgn-utils/src/actions/actions.ts | 3 +- .../mrgn-utils/src/actions/individualFlows.ts | 54 +++++++++++++------ packages/mrgn-utils/src/actions/types.ts | 3 ++ 10 files changed, 82 insertions(+), 51 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx index 657a74f904..a32ec3362f 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/stats.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ArenaPoolV2Extended } from "~/store/tradeStoreV2"; import { generateTradeStats } from "./utils/stats-utils"; -import { cn, LoopActionTxns } from "@mrgnlabs/mrgn-utils"; +import { cn, TradeActionTxns } from "@mrgnlabs/mrgn-utils"; import { SimulationResult } from "@mrgnlabs/marginfi-client-v2"; import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; import { ActionStatItem } from "~/components/action-box-v2/components/action-stats/action-stat-item"; @@ -10,7 +10,7 @@ interface StatsProps { activePool: ArenaPoolV2Extended; accountSummary: AccountSummary | null; simulationResult: SimulationResult | null; - actionTxns: LoopActionTxns | null; + actionTxns: TradeActionTxns | null; } export const Stats = ({ activePool, accountSummary, simulationResult, actionTxns }: StatsProps) => { const stats = React.useMemo( diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx index 714ff4cfdb..cac6465e58 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/stats/utils/stats-utils.tsx @@ -1,7 +1,7 @@ import { MarginRequirementType, SimulationResult } from "@mrgnlabs/marginfi-client-v2"; import { AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; import { percentFormatter, tokenPriceFormatter, usdFormatter } from "@mrgnlabs/mrgn-common"; -import { LoopActionTxns } from "@mrgnlabs/mrgn-utils"; +import { TradeActionTxns } from "@mrgnlabs/mrgn-utils"; import Link from "next/link"; import { PreviewStat } from "~/components/action-box-v2/utils"; import { IconPyth } from "~/components/ui/icons"; @@ -13,7 +13,7 @@ interface generateTradeStatsProps { accountSummary: AccountSummary | null; extendedPool: ArenaPoolV2Extended; simulationResult: SimulationResult | null; - actionTxns: LoopActionTxns | null; + actionTxns: TradeActionTxns | null; } export function generateTradeStats(props: generateTradeStatsProps) { diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index 4e5363e0b1..427587f9a2 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -11,7 +11,7 @@ import { CalculateLoopingProps, DYNAMIC_SIMULATION_ERRORS, extractErrorString, - LoopActionTxns, + TradeActionTxns, STATIC_SIMULATION_ERRORS, usePrevious, } from "@mrgnlabs/mrgn-utils"; @@ -29,7 +29,7 @@ export type TradeSimulationProps = { selectedBank: ArenaBank | null; selectedSecondaryBank: ArenaBank | null; marginfiClient: MarginfiClient | null; - actionTxns: LoopActionTxns; + actionTxns: TradeActionTxns; simulationResult: SimulationResult | null; wrappedAccount: MarginfiAccountWrapper | null; accountSummary?: AccountSummary; @@ -38,7 +38,7 @@ export type TradeSimulationProps = { slippageBps: number; platformFeeBps: number; - setActionTxns: (actionTxns: LoopActionTxns) => void; + setActionTxns: (actionTxns: TradeActionTxns) => void; setErrorMessage: (error: ActionMessageType | null) => void; setIsLoading: ({ isLoading, status }: { isLoading: boolean; status: SimulationStatus }) => void; setSimulationResult: (result: SimulationResult | null) => void; @@ -73,7 +73,7 @@ export function useTradeSimulation({ callbacks: { setErrorMessage: (error: ActionMessageType | null) => void; setSimulationResult: (result: SimulationResult | null) => void; - setActionTxns: (actionTxns: LoopActionTxns) => void; + setActionTxns: (actionTxns: TradeActionTxns) => void; setIsLoading: ({ isLoading, status }: { isLoading: boolean; status: SimulationStatus }) => void; } ) => { @@ -129,7 +129,7 @@ export function useTradeSimulation({ const fetchTradeTxnsAction = async ( props: CalculateLoopingProps - ): Promise<{ actionTxns: LoopActionTxns | null; actionMessage: ActionMessageType | null }> => { + ): Promise<{ actionTxns: TradeActionTxns | null; actionMessage: ActionMessageType | null }> => { try { const loopingResult = await generateTradeTx({ ...props, @@ -166,7 +166,7 @@ export function useTradeSimulation({ } setIsLoading({ isLoading: true, status: SimulationStatus.SIMULATING }); - const loopActionTxns = await fetchTradeTxnsAction({ + const tradeActionTxns = await fetchTradeTxnsAction({ marginfiClient: marginfiClient, marginfiAccount: wrappedAccount, depositBank: selectedBank, @@ -178,8 +178,10 @@ export function useTradeSimulation({ platformFeeBps: platformFeeBps, }); - if (loopActionTxns.actionMessage || loopActionTxns.actionTxns === null) { - handleError(loopActionTxns.actionMessage ?? STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED, { + console.log("tradeActionTxns", tradeActionTxns); + + if (tradeActionTxns.actionMessage || tradeActionTxns.actionTxns === null) { + handleError(tradeActionTxns.actionMessage ?? STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED, { // TODO: update error message setErrorMessage, setSimulationResult, @@ -189,12 +191,15 @@ export function useTradeSimulation({ return; } - if (!loopActionTxns.actionTxns.accountCreationTx) { - setActionTxns(loopActionTxns.actionTxns); + // TODO: this has to change + + if (!tradeActionTxns.actionTxns.accountCreationTx) { + setActionTxns(tradeActionTxns.actionTxns); return; } if (!wrappedAccount) { + console.log("wrappedAccount is null"); // throw error return; } @@ -203,8 +208,8 @@ export function useTradeSimulation({ account: wrappedAccount, bank: selectedBank, txns: [ - ...(loopActionTxns?.actionTxns?.additionalTxns ?? []), - ...(loopActionTxns?.actionTxns?.actionTxn ? [loopActionTxns?.actionTxns?.actionTxn] : []), + ...(tradeActionTxns?.actionTxns?.additionalTxns ?? []), + ...(tradeActionTxns?.actionTxns?.actionTxn ? [tradeActionTxns?.actionTxns?.actionTxn] : []), ], }); @@ -219,7 +224,7 @@ export function useTradeSimulation({ return; } else if (simulationResult.simulationResult) { setSimulationResult(simulationResult.simulationResult); - setActionTxns(loopActionTxns.actionTxns); + setActionTxns(tradeActionTxns.actionTxns); } else { throw new Error("Unknown error"); } @@ -243,7 +248,6 @@ export function useTradeSimulation({ selectedSecondaryBank, marginfiClient, setIsLoading, - fetchTradeTxnsAction, wrappedAccount, slippageBps, platformFeeBps, diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx index 0bcc084681..73cb7991ea 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/store/trade-box-store.tsx @@ -1,8 +1,7 @@ -import { ActionMessageType, calculateLstYield, LSTS_SOLANA_COMPASS_MAP } from "@mrgnlabs/mrgn-utils"; +import { ActionMessageType, TradeActionTxns } from "@mrgnlabs/mrgn-utils"; import BigNumber from "bignumber.js"; import { SimulationResult } from "@mrgnlabs/marginfi-client-v2"; -import { LoopActionTxns } from "@mrgnlabs/mrgn-utils"; import { create, StateCreator } from "zustand"; import { TradeSide } from ".."; import { ArenaBank } from "~/store/tradeStoreV2"; @@ -18,7 +17,7 @@ interface TradeBoxState { selectedSecondaryBank: ArenaBank | null; simulationResult: SimulationResult | null; - actionTxns: LoopActionTxns; + actionTxns: TradeActionTxns; errorMessage: ActionMessageType | null; @@ -29,7 +28,7 @@ interface TradeBoxState { setTradeState: (tradeState: TradeSide) => void; setLeverage: (leverage: number) => void; setSimulationResult: (result: SimulationResult | null) => void; - setActionTxns: (actionTxns: LoopActionTxns) => void; + setActionTxns: (actionTxns: TradeActionTxns) => void; setErrorMessage: (errorMessage: ActionMessageType | null) => void; setSelectedBank: (bank: ArenaBank | null) => void; setSelectedSecondaryBank: (bank: ArenaBank | null) => void; @@ -113,7 +112,7 @@ const stateCreator: StateCreator = (set, get) => ({ set({ simulationResult: result }); }, - setActionTxns(actionTxns: LoopActionTxns) { + setActionTxns(actionTxns: TradeActionTxns) { set({ actionTxns: actionTxns }); }, diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx index e27a1d3bd4..2b70299e57 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/trade-box.tsx @@ -9,7 +9,7 @@ import { ExecuteTradeActionProps, formatAmount, IndividualFlowError, - LoopActionTxns, + TradeActionTxns, MultiStepToastHandle, showErrorToast, useConnection, @@ -259,7 +259,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { onComplete?: (txn: PreviousTxn) => void; setIsLoading: (isLoading: boolean) => void; setAmountRaw: (amountRaw: string) => void; - retryCallback: (txs: ActionTxns, toast: MultiStepToastHandle) => void; + retryCallback: (txs: TradeActionTxns, toast: MultiStepToastHandle) => void; } ) => { const action = async (params: ExecuteTradeActionProps) => { @@ -269,7 +269,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { callbacks.captureEvent && callbacks.captureEvent(event, properties); }, setIsComplete: (txnSigs) => { - const _actionTxns = params.actionTxns as LoopActionTxns; + const _actionTxns = params.actionTxns as TradeActionTxns; callbacks.setIsActionComplete(true); callbacks.setPreviousTxn({ txnType: "TRADING", @@ -309,7 +309,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { if (!toast) { return; } - const txs = error.actionTxns as ActionTxns; + const txs = error.actionTxns as TradeActionTxns; let retry = undefined; if (error.retry && toast && txs) { retry = () => callbacks.retryCallback(txs, toast); @@ -345,7 +345,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { }, setIsLoading: setIsTransactionExecuting, setAmountRaw, - retryCallback: (txns: ActionTxns, multiStepToast: MultiStepToastHandle) => { + retryCallback: (txns: TradeActionTxns, multiStepToast: MultiStepToastHandle) => { retryTradeAction({ ...params, actionTxns: txns, multiStepToast }, leverage); }, }); @@ -406,7 +406,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { }, setIsLoading: setIsTransactionExecuting, setAmountRaw, - retryCallback: (txns: ActionTxns, multiStepToast: MultiStepToastHandle) => { + retryCallback: (txns: TradeActionTxns, multiStepToast: MultiStepToastHandle) => { retryTradeAction({ ...params, actionTxns: txns, multiStepToast }, leverage); }, }); diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts index 407afaab47..8d69a0bdea 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts @@ -4,13 +4,13 @@ import { executeTradeAction, ExecuteTradeActionProps, CalculateLoopingProps, - LoopActionTxns, ActionMessageType, calculateLoopingParams, + TradeActionTxns, } from "@mrgnlabs/mrgn-utils"; import { ExecuteActionsCallbackProps } from "~/components/action-box-v2/types"; -import { Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { Keypair, PublicKey } from "@solana/web3.js"; import { BalanceRaw, MarginfiAccount, @@ -68,7 +68,7 @@ interface GenerateTradeTxProps extends CalculateLoopingProps { authority: PublicKey; } -export async function generateTradeTx(props: GenerateTradeTxProps): Promise { +export async function generateTradeTx(props: GenerateTradeTxProps): Promise { const hasMarginfiAccount = !!props.marginfiAccount; let accountCreationTx: SolanaTransaction | undefined; diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts index 70867fd9e2..05a4c5ed13 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-simulation.utils.ts @@ -1,6 +1,6 @@ import { SimulationResult } from "@mrgnlabs/marginfi-client-v2"; -import { ActionMessageType, handleSimulationError, LoopActionTxns } from "@mrgnlabs/mrgn-utils"; +import { ActionMessageType, handleSimulationError, TradeActionTxns } from "@mrgnlabs/mrgn-utils"; import { ArenaBank } from "~/store/tradeStoreV2"; import { @@ -52,7 +52,7 @@ export function calculateSummary({ simulationResult?: SimulationResult; bank: ArenaBank; accountSummary: AccountSummary; - actionTxns: LoopActionTxns; + actionTxns: TradeActionTxns; }): ActionSummary { let simulationPreview: SimulatedActionPreview | null = null; @@ -92,7 +92,7 @@ export function calculateSimulatedActionPreview( function calculateActionPreview( bank: ArenaBank, accountSummary: AccountSummary, - actionTxns: LoopActionTxns + actionTxns: TradeActionTxns ): ActionPreview { const positionAmount = bank?.isActive ? bank.position.amount : 0; const health = accountSummary.balance && accountSummary.healthFactor ? accountSummary.healthFactor : 1; diff --git a/packages/mrgn-utils/src/actions/actions.ts b/packages/mrgn-utils/src/actions/actions.ts index 9fde010669..7386ccb362 100644 --- a/packages/mrgn-utils/src/actions/actions.ts +++ b/packages/mrgn-utils/src/actions/actions.ts @@ -12,6 +12,7 @@ import { RepayWithCollatProps, LoopingProps, LoopActionTxns, + TradeActionTxns, } from "./types"; import { WalletContextStateOverride } from "../wallet"; import { @@ -135,7 +136,7 @@ export async function executeLoopingAction(params: ExecuteLoopingActionProps) { export interface ExecuteTradeActionProps extends LoopingProps { marginfiClient: MarginfiClient; - actionTxns: LoopActionTxns; + actionTxns: TradeActionTxns; processOpts: ProcessTransactionsClientOpts; txOpts: TransactionOptions; tradeSide: "long" | "short"; diff --git a/packages/mrgn-utils/src/actions/individualFlows.ts b/packages/mrgn-utils/src/actions/individualFlows.ts index 8306812c5c..79a91102b4 100644 --- a/packages/mrgn-utils/src/actions/individualFlows.ts +++ b/packages/mrgn-utils/src/actions/individualFlows.ts @@ -42,7 +42,7 @@ import { ActionTxns, RepayWithCollatProps, IndividualFlowError, - LoopActionTxns, + TradeActionTxns, } from "./types"; import { captureSentryException } from "../sentry.utils"; import { loopingBuilder, repayWithCollatBuilder } from "./flashloans"; @@ -52,13 +52,24 @@ import { handleError } from "../errors"; // Local utils functions // //-----------------------// -export function getSteps(actionTxns?: ActionTxns) { - return [ - { label: "Signing transaction" }, - ...(actionTxns?.additionalTxns.map((tx) => ({ - label: MRGN_TX_TYPE_TOAST_MAP[tx.type ?? "CRANK"], - })) ?? []), - ]; +export function getSteps(actionTxns?: ActionTxns, broadcastType?: TransactionBroadcastType) { + const steps = []; + + steps.push({ label: "Signing transaction" }); + + if (actionTxns && typeof actionTxns === "object" && "accountCreationTx" in actionTxns) { + steps.push({ label: "Creating marginfi account" }); + + if (broadcastType !== "RPC") { + steps.push({ label: "Signing transaction" }); + } + } + + actionTxns?.additionalTxns.forEach((tx) => { + steps.push({ label: MRGN_TX_TYPE_TOAST_MAP[tx.type ?? "CRANK"] }); + }); + + return steps; } export function composeExplorerUrl(signature?: string, broadcastType: TransactionBroadcastType = "RPC") { @@ -590,7 +601,7 @@ export async function looping({ interface TradeFnProps extends LoopingProps { marginfiClient: MarginfiClient; - actionTxns: LoopActionTxns; + actionTxns: TradeActionTxns; processOpts: ProcessTransactionsClientOpts; txOpts: TransactionOptions; tradeSide: "long" | "short"; @@ -609,11 +620,6 @@ export async function trade({ return; } - if (actionTxns?.accountCreationTx) { - const accountCreationTx = actionTxns.accountCreationTx; - //process seperatetly then execute next - } - if (!multiStepToast) { const steps = getSteps(actionTxns); @@ -636,8 +642,26 @@ export async function trade({ let sigs: string[] = []; if (actionTxns?.actionTxn) { + const txns: SolanaTransaction[] = [...actionTxns.additionalTxns, actionTxns.actionTxn]; + if (actionTxns.accountCreationTx) { + if (processOpts.broadcastType !== "RPC") { + await marginfiClient.processTransaction(actionTxns.accountCreationTx, { + ...processOpts, + callback: (index, success, sig, stepsToAdvance) => + success && + multiStepToast.setSuccessAndNext( + stepsToAdvance, + sig, + composeExplorerUrl(sig, processOpts?.broadcastType) + ), + }); // TODO: add sig saving & displaying + } else { + txns.push(actionTxns.accountCreationTx); + } + } + sigs = await marginfiClient.processTransactions( - [...actionTxns.additionalTxns, actionTxns.actionTxn], + txns, { ...processOpts, callback: (index, success, sig, stepsToAdvance) => diff --git a/packages/mrgn-utils/src/actions/types.ts b/packages/mrgn-utils/src/actions/types.ts index ce972927b8..17f4ed9618 100644 --- a/packages/mrgn-utils/src/actions/types.ts +++ b/packages/mrgn-utils/src/actions/types.ts @@ -74,6 +74,9 @@ export interface LoopActionTxns extends ActionTxns { lastValidBlockHeight?: number; actualDepositAmount: number; borrowAmount: BigNumber; +} + +export interface TradeActionTxns extends LoopActionTxns { accountCreationTx?: SolanaTransaction; marginfiAccount?: MarginfiAccountWrapper; } From fbc32c4df6811d2a6164ca36aa963277c3344063 Mon Sep 17 00:00:00 2001 From: Kobe Date: Fri, 13 Dec 2024 23:02:14 +0100 Subject: [PATCH 24/27] fix: account tx not being set --- .../common/trade-box-v2/hooks/use-trade-simulation.ts | 10 ++-------- .../common/trade-box-v2/utils/trade-action.utils.ts | 1 - packages/marginfi-client-v2/src/clients/client.ts | 1 - 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index 427587f9a2..0d8caf367f 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -178,8 +178,6 @@ export function useTradeSimulation({ platformFeeBps: platformFeeBps, }); - console.log("tradeActionTxns", tradeActionTxns); - if (tradeActionTxns.actionMessage || tradeActionTxns.actionTxns === null) { handleError(tradeActionTxns.actionMessage ?? STATIC_SIMULATION_ERRORS.REPAY_COLLAT_FAILED, { // TODO: update error message @@ -191,17 +189,13 @@ export function useTradeSimulation({ return; } - // TODO: this has to change - - if (!tradeActionTxns.actionTxns.accountCreationTx) { + if (tradeActionTxns.actionTxns.accountCreationTx) { setActionTxns(tradeActionTxns.actionTxns); return; } if (!wrappedAccount) { - console.log("wrappedAccount is null"); - // throw error - return; + throw new Error("Marginfi account is null"); } const simulationResult = await simulationAction({ diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts index 8d69a0bdea..5999ee4690 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts @@ -76,7 +76,6 @@ export async function generateTradeTx(props: GenerateTradeTxProps): Promise Date: Sat, 14 Dec 2024 11:51:59 +0100 Subject: [PATCH 25/27] fix: small improvements & refactor --- .../info-messages/info-messages.tsx | 4 +- .../hooks/use-trade-simulation.ts | 7 ---- .../common/trade-box-v2/trade-box.tsx | 28 +++++++------- .../trade-box-v2/utils/trade-action.utils.ts | 12 +++--- .../trade-box-v2/utils/trade-box.utils.ts | 38 +++++++++++++------ .../src/pages/trade/[symbol].tsx | 4 +- 6 files changed, 50 insertions(+), 43 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx index e98d1b2be7..5e4c73733a 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/components/info-messages/info-messages.tsx @@ -14,7 +14,6 @@ interface InfoMessagesProps { activePool: ArenaPoolV2Extended; isActiveWithCollat: boolean; actionMethods: ActionMessageType[]; - additionalChecks?: ActionMessageType[]; setIsWalletOpen: (value: boolean) => void; fetchTradeState: ({ connection, @@ -37,7 +36,6 @@ export const InfoMessages = ({ activePool, isActiveWithCollat, actionMethods = [], - additionalChecks, setIsWalletOpen, fetchTradeState, connection, @@ -79,7 +77,7 @@ export const InfoMessages = ({ const renderActionMethodMessages = () => (
- {actionMethods.concat(additionalChecks ?? []).map( + {actionMethods.map( (actionMethod, idx) => actionMethod.description && (
{ const debouncedLeverage = useAmountDebounce(leverage, 500); // States - const [additionalActionMessages, setAdditionalActionMessages] = React.useState([]); + const [dynamicActionMessages, setDynamicActionMessages] = React.useState([]); // Loading states const [isTransactionExecuting, setIsTransactionExecuting] = React.useState(false); @@ -163,7 +163,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { } }, [tradeState, actionTxns]); - const actionMethods = React.useMemo( + const staticActionMethods = React.useMemo( () => checkTradeActionAvailable({ amount, @@ -177,11 +177,15 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { [amount, connected, actionTxns, tradeState, selectedSecondaryBank, selectedBank] ); + const actionMethods = React.useMemo(() => { + return staticActionMethods.concat(dynamicActionMessages); + }, [staticActionMethods, dynamicActionMessages]); + const isDisabled = React.useMemo(() => { - if (!actionTxns?.actionQuote) return true; - if (actionMethods.concat(additionalActionMessages).filter((value) => value.isEnabled === false).length) return true; + if (!actionTxns?.actionQuote || !actionTxns?.actionTxn) return true; + if (actionMethods.filter((value) => value.isEnabled === false).length) return true; return false; - }, [actionMethods, additionalActionMessages, actionTxns]); + }, [actionMethods, actionTxns]); // Effects React.useEffect(() => { @@ -201,9 +205,9 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { if (errorMessage.actionMethod === "ERROR") { showErrorToast(errorMessage?.description); } - setAdditionalActionMessages([errorMessage]); + setDynamicActionMessages([errorMessage]); } else { - setAdditionalActionMessages([]); + setDynamicActionMessages([]); } }, [errorMessage]); @@ -224,10 +228,7 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { wrappedAccount: wrappedAccount, slippageBps: slippageBps, platformFeeBps: platformFeeBps, - actionTxns: actionTxns, - simulationResult: null, - accountSummary: accountSummary ?? undefined, - isEnabled: !actionMethods.concat(additionalActionMessages).filter((value) => value.isEnabled === false).length, + isEnabled: !actionMethods.filter((value) => value.isEnabled === false).length, setActionTxns: setActionTxns, setErrorMessage: setErrorMessage, setIsLoading: setIsSimulating, @@ -465,14 +466,13 @@ export const TradeBoxV2 = ({ activePool, side = "long" }: TradeBoxV2Props) => { selectedBank={activePoolExtended.tokenBank} /> )} - {actionMethods && actionMethods.concat(additionalActionMessages).some((method) => method.description) && ( + {actionMethods && actionMethods.some((method) => method.description) && ( {
0} + hasErrorMessages={dynamicActionMessages.length > 0} isActive={selectedBank && amount > 0 ? true : false} /> { +export async function generateTradeTx(props: CalculateLoopingProps): Promise { const hasMarginfiAccount = !!props.marginfiAccount; let accountCreationTx: SolanaTransaction | undefined; @@ -76,6 +72,9 @@ export async function generateTradeTx(props: GenerateTradeTxProps): Promise
- {(!initialized || !poolsFetched || !activePool) && } - {initialized && poolsFetched && activePool && ( + {!activePool && } + {activePool && (
From 600510fed22461ddef2028124da7ab6ae7907ac7 Mon Sep 17 00:00:00 2001 From: Kobe Date: Sat, 14 Dec 2024 12:21:46 +0100 Subject: [PATCH 26/27] fix: added marginfi account creation --- .../common/trade-box-v2/hooks/use-trade-simulation.ts | 9 +++------ .../common/trade-box-v2/utils/trade-action.utils.ts | 11 ++++++----- packages/mrgn-utils/src/actions/types.ts | 1 - 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts index 45a51f5222..fa6e76035b 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/hooks/use-trade-simulation.ts @@ -182,17 +182,14 @@ export function useTradeSimulation({ return; } - if (tradeActionTxns.actionTxns.accountCreationTx) { - setActionTxns(tradeActionTxns.actionTxns); - return; - } + const finalAccount = tradeActionTxns?.actionTxns.marginfiAccount || wrappedAccount; - if (!wrappedAccount) { + if (!finalAccount) { throw new Error("Marginfi account is null"); } const simulationResult = await simulationAction({ - account: wrappedAccount, + account: finalAccount, bank: selectedBank, txns: [ ...(tradeActionTxns?.actionTxns?.additionalTxns ?? []), diff --git a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts index 78fff7cc51..fadb2731b5 100644 --- a/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts +++ b/apps/marginfi-v2-trading/src/components/common/trade-box-v2/utils/trade-action.utils.ts @@ -66,7 +66,7 @@ export const handleExecuteTradeAction = async ({ export async function generateTradeTx(props: CalculateLoopingProps): Promise { const hasMarginfiAccount = !!props.marginfiAccount; - let accountCreationTx: SolanaTransaction | undefined; + let accountCreationTx: SolanaTransaction[] = []; let finalAccount: MarginfiAccountWrapper | null = props.marginfiAccount; @@ -101,16 +101,17 @@ export async function generateTradeTx(props: CalculateLoopingProps): Promise Date: Sat, 14 Dec 2024 12:41:19 +0100 Subject: [PATCH 27/27] fix: ata creation missing due to missing metadata --- .../src/hooks/useMarginfiClient.tsx | 44 ++++++++++--------- .../src/models/account/wrapper.ts | 1 - 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/apps/marginfi-v2-trading/src/hooks/useMarginfiClient.tsx b/apps/marginfi-v2-trading/src/hooks/useMarginfiClient.tsx index 5af4afc98e..144fdb598e 100644 --- a/apps/marginfi-v2-trading/src/hooks/useMarginfiClient.tsx +++ b/apps/marginfi-v2-trading/src/hooks/useMarginfiClient.tsx @@ -16,6 +16,7 @@ import React from "react"; import { useConnection } from "~/hooks/use-connection"; import { useTradeStoreV2 } from "~/store"; import { ArenaBank } from "~/store/tradeStoreV2"; +import { BankMetadata } from "@mrgnlabs/mrgn-common"; type UseMarginfiClientProps = { groupPk: PublicKey; @@ -34,25 +35,16 @@ export function useMarginfiClient({ programId: defaultConfig.programId, }, }: UseMarginfiClientProps) { - const [ - arenaPools, - banksByBankPk, - groupsByGroupPk, - tokenAccountMap, - lutByGroupPk, - mintDataByMint, - bankMetadataCache, - wallet, - ] = useTradeStoreV2((state) => [ - state.arenaPools, - state.banksByBankPk, - state.groupsByGroupPk, - state.tokenAccountMap, - state.lutByGroupPk, - state.mintDataByMint, - state.bankMetadataCache, - state.wallet, - ]); + const [arenaPools, banksByBankPk, groupsByGroupPk, tokenAccountMap, lutByGroupPk, mintDataByMint, wallet] = + useTradeStoreV2((state) => [ + state.arenaPools, + state.banksByBankPk, + state.groupsByGroupPk, + state.tokenAccountMap, + state.lutByGroupPk, + state.mintDataByMint, + state.wallet, + ]); const { connection } = useConnection(); const client = React.useMemo(() => { @@ -99,6 +91,17 @@ export function useMarginfiClient({ const program = new Program(idl, provider) as any as MarginfiProgram; + let bankMetadataByBankPk: Record = {}; + + [tokenBank, quoteBank].forEach((bank) => { + const bankPk = bank.info.rawBank.address.toBase58(); + bankMetadataByBankPk[bankPk] = { + tokenAddress: bank.info.state.mint.toBase58(), + tokenName: bank.meta.tokenName, + tokenSymbol: bank.meta.tokenSymbol, + }; + }); + const client = new MarginfiClient( { groupPk, ...clientConfig }, program, @@ -111,7 +114,7 @@ export function useMarginfiClient({ feedIdMap, lut, bankAddresses, - bankMetadataCache, + bankMetadataByBankPk, clientOptions?.bundleSimRpcEndpoint, clientOptions?.processTransactionStrategy ); @@ -132,7 +135,6 @@ export function useMarginfiClient({ clientOptions?.processTransactionStrategy, wallet, clientConfig, - bankMetadataCache, ]); return client; diff --git a/packages/marginfi-client-v2/src/models/account/wrapper.ts b/packages/marginfi-client-v2/src/models/account/wrapper.ts index 9fc114e191..4d287a949a 100644 --- a/packages/marginfi-client-v2/src/models/account/wrapper.ts +++ b/packages/marginfi-client-v2/src/models/account/wrapper.ts @@ -870,7 +870,6 @@ class MarginfiAccountWrapper { txs: (VersionedTransaction | Transaction)[], banksToInspect: PublicKey[] ): Promise { - console.log("txs", txs); const [mfiAccountData, ...bankData] = await this.client.simulateTransactions(txs, [ this.address, ...banksToInspect,