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 2cd140821..ceed58ba3 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 777ba1a0e..927fbcc67 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 9f0fa2b3b..967b8b1f2 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 bf5d4573c..959d28062 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;