diff --git a/apps/marginfi-v2-trading/src/components/common/Header/Header.tsx b/apps/marginfi-v2-trading/src/components/common/Header/Header.tsx index 60e03439c5..2228c18eb3 100644 --- a/apps/marginfi-v2-trading/src/components/common/Header/Header.tsx +++ b/apps/marginfi-v2-trading/src/components/common/Header/Header.tsx @@ -5,24 +5,26 @@ import React from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { motion, useAnimate } from "framer-motion"; -import { IconPlus, IconCopy, IconCheck } from "@tabler/icons-react"; +import { IconPlus, IconCopy, IconCheck, IconSettings } from "@tabler/icons-react"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { cn } from "@mrgnlabs/mrgn-utils"; import { USDC_MINT } from "@mrgnlabs/mrgn-common"; +import { Settings } from "@mrgnlabs/mrgn-ui"; -import { useTradeStore } from "~/store"; +import { useTradeStore, useUiStore } from "~/store"; import { useWallet } from "~/components/wallet-v2/hooks/use-wallet.hook"; import { useIsMobile } from "~/hooks/use-is-mobile"; import { useConnection } from "~/hooks/use-connection"; import { Wallet } from "~/components/wallet-v2"; -import { CreatePoolScriptDialog } from "../Pool/CreatePoolScript"; -import { CreatePoolSoon } from "../Pool/CreatePoolSoon"; -import { CreatePoolDialog } from "../Pool/CreatePoolDialog"; import { Button } from "~/components/ui/button"; import { IconArena } from "~/components/ui/icons"; +import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; +import { CreatePoolScriptDialog } from "../Pool/CreatePoolScript"; +import { CreatePoolSoon } from "../Pool/CreatePoolSoon"; + const navItems = [ { label: "Discover", href: "/" }, { label: "Yield", href: "/yield" }, @@ -41,6 +43,13 @@ export const Header = () => { state.referralCode, ] ); + const { priorityType, broadcastType, maxCap, maxCapType, setTransactionSettings } = useUiStore((state) => ({ + priorityType: state.priorityType, + broadcastType: state.broadcastType, + maxCap: state.maxCap, + maxCapType: state.maxCapType, + setTransactionSettings: state.setTransactionSettings, + })); const { wallet } = useWallet(); const { asPath } = useRouter(); const isMobile = useIsMobile(); @@ -140,6 +149,22 @@ export const Header = () => { /> */} )} + + + + + + + + [state.fetchTradeState]); - const [priorityFee] = useUiStore((state) => [state.priorityFee]); const [activeStep, setActiveStep] = React.useState(0); const [status, setStatus] = React.useState("default"); diff --git a/apps/marginfi-v2-trading/src/components/common/Pool/CreatePoolScript/components/CreatePoolLoading.tsx b/apps/marginfi-v2-trading/src/components/common/Pool/CreatePoolScript/components/CreatePoolLoading.tsx index e3af9c7f53..6f0d067390 100644 --- a/apps/marginfi-v2-trading/src/components/common/Pool/CreatePoolScript/components/CreatePoolLoading.tsx +++ b/apps/marginfi-v2-trading/src/components/common/Pool/CreatePoolScript/components/CreatePoolLoading.tsx @@ -134,7 +134,6 @@ type PoolCreationState = { export const CreatePoolLoading = ({ poolCreatedData, setIsOpen, setIsCompleted }: CreatePoolLoadingProps) => { // const { wallet } = useWallet(); const { connection } = useConnection(); - const [priorityFee] = useUiStore((state) => [state.priorityFee]); const [activeStep, setActiveStep] = React.useState(0); const [status, setStatus] = React.useState("default"); @@ -208,7 +207,7 @@ export const CreatePoolLoading = ({ poolCreatedData, setIsOpen, setIsCompleted } group, admin: wallet.publicKey, seed, - priorityFee, + priorityFee: 0, // todo }); if (!sig) throw new Error(); @@ -218,7 +217,7 @@ export const CreatePoolLoading = ({ poolCreatedData, setIsOpen, setIsCompleted } setStatus("error"); } }, - [priorityFee, setStatus] + [setStatus] ); const createSeeds = React.useCallback(() => { diff --git a/apps/marginfi-v2-trading/src/components/common/Portfolio/PositionActionButtons.tsx b/apps/marginfi-v2-trading/src/components/common/Portfolio/PositionActionButtons.tsx index 1bce8a441b..40cd5cd6ce 100644 --- a/apps/marginfi-v2-trading/src/components/common/Portfolio/PositionActionButtons.tsx +++ b/apps/marginfi-v2-trading/src/components/common/Portfolio/PositionActionButtons.tsx @@ -5,7 +5,7 @@ import Image from "next/image"; import { IconMinus, IconX, IconPlus, IconLoader2 } from "@tabler/icons-react"; import { Transaction, VersionedTransaction } from "@solana/web3.js"; -import { MultiStepToastHandle, cn, extractErrorString, capture } from "@mrgnlabs/mrgn-utils"; +import { MultiStepToastHandle, cn, extractErrorString, capture, fetchPriorityFee } from "@mrgnlabs/mrgn-utils"; import { ActiveBankInfo, ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; import { useConnection } from "~/hooks/use-connection"; @@ -56,12 +56,16 @@ export const PositionActionButtons = ({ state.setIsRefreshingStore, state.nativeSolBalance, ]); - const [slippageBps, priorityFee, setIsActionComplete, setPreviousTxn] = useUiStore((state) => [ - state.slippageBps, - state.priorityFee, - state.setIsActionComplete, - state.setPreviousTxn, - ]); + const [slippageBps, priorityType, broadcastType, maxCap, maxCapType, setIsActionComplete, setPreviousTxn] = + useUiStore((state) => [ + state.slippageBps, + state.priorityType, + state.broadcastType, + state.maxCap, + state.maxCapType, + state.setIsActionComplete, + state.setPreviousTxn, + ]); const depositBanks = React.useMemo(() => { const tokenBank = activeGroup.pool.token.isActive ? activeGroup.pool.token : null; @@ -103,13 +107,15 @@ export const PositionActionButtons = ({ throw new Error("Invalid client"); } + const priorityFeeUi = await fetchPriorityFee(maxCapType, maxCap, broadcastType, priorityType, connection); + const txns = await calculateClosePositions({ marginfiAccount: activeGroup.selectedAccount, depositBanks: depositBanks, borrowBank: borrowBank, slippageBps, connection: connection, - priorityFee, + priorityFee: priorityFeeUi, platformFeeBps, }); @@ -128,7 +134,18 @@ export const PositionActionButtons = ({ setMultiStepToast(multiStepToast); } setIsClosing(false); - }, [activeGroup, slippageBps, connection, priorityFee, platformFeeBps, borrowBank, depositBanks, setIsClosing]); + }, [ + activeGroup, + borrowBank, + depositBanks, + maxCapType, + maxCap, + broadcastType, + priorityType, + connection, + slippageBps, + platformFeeBps, + ]); const processTransaction = React.useCallback(async () => { try { @@ -140,10 +157,13 @@ export const PositionActionButtons = ({ txnSig = await activeGroup.client.processTransaction(actionTransaction.closeTxn); multiStepToast.setSuccessAndNext(); } else { - txnSig = await activeGroup.client.processTransactions([ - ...actionTransaction.feedCrankTxs, - actionTransaction.closeTxn, - ]); + txnSig = await activeGroup.client.processTransactions( + [...actionTransaction.feedCrankTxs, actionTransaction.closeTxn], + undefined, + undefined, + broadcastType, + true + ); multiStepToast.setSuccessAndNext(); } @@ -202,6 +222,7 @@ export const PositionActionButtons = ({ connection, setIsActionComplete, setPreviousTxn, + broadcastType, ]); const onClose = React.useCallback(() => { diff --git a/apps/marginfi-v2-trading/src/components/common/TradingBox/TradingBox.tsx b/apps/marginfi-v2-trading/src/components/common/TradingBox/TradingBox.tsx index 13d2539f93..8d08c2015b 100644 --- a/apps/marginfi-v2-trading/src/components/common/TradingBox/TradingBox.tsx +++ b/apps/marginfi-v2-trading/src/components/common/TradingBox/TradingBox.tsx @@ -15,6 +15,7 @@ import { capture, extractErrorString, usePrevious, + usePriorityFee, } from "@mrgnlabs/mrgn-utils"; import { MarginfiAccountWrapper, SimulationResult, computeMaxLeverage } from "@mrgnlabs/marginfi-client-v2"; import { IconAlertTriangle, IconExternalLink, IconLoader2, IconSettings, IconWallet } from "@tabler/icons-react"; @@ -75,16 +76,29 @@ export const TradingBox = ({ activeGroup, side = "long" }: TradingBoxProps) => { state.refreshGroup, ]); - const [slippageBps, priorityFee, platformFeeBps, setSlippageBps, setIsActionComplete, setPreviousTxn] = useUiStore( - (state) => [ - state.slippageBps, - state.priorityFee, - state.platformFeeBps, - state.setSlippageBps, - state.setIsActionComplete, - state.setPreviousTxn, - ] - ); + const [ + slippageBps, + platformFeeBps, + priorityType, + broadcastType, + maxCap, + maxCapType, + setSlippageBps, + setIsActionComplete, + setPreviousTxn, + ] = useUiStore((state) => [ + state.slippageBps, + state.platformFeeBps, + state.priorityType, + state.broadcastType, + state.maxCap, + state.maxCapType, + state.setSlippageBps, + state.setIsActionComplete, + state.setPreviousTxn, + ]); + + const priorityFee = usePriorityFee(priorityType, broadcastType, maxCapType, maxCap, connection); const [setIsWalletOpen] = useWalletStore((state) => [state.setIsWalletOpen]); @@ -236,6 +250,7 @@ export const TradingBox = ({ activeGroup, side = "long" }: TradingBoxProps) => { connection, platformFeeBps, isTrading: true, + broadcastType: broadcastType, }); let loopingObject: LoopingObject | null = null; @@ -263,10 +278,11 @@ export const TradingBox = ({ activeGroup, side = "long" }: TradingBoxProps) => { amount, leverage, tradeState, - slippageBps, priorityFee, + slippageBps, connection, platformFeeBps, + broadcastType, handleSimulation, ]); @@ -298,12 +314,23 @@ export const TradingBox = ({ activeGroup, side = "long" }: TradingBoxProps) => { loopingObject, priorityFee, slippageBps: slippageBps, + broadcastType: broadcastType, connection, }); return sig; }, - [amount, connection, loopingObject, priorityFee, activeGroup, slippageBps, tradeState, walletContextState] + [ + amount, + connection, + loopingObject, + priorityFee, + activeGroup, + slippageBps, + tradeState, + broadcastType, + walletContextState, + ] ); const handleLeverageAction = React.useCallback(async () => { @@ -547,24 +574,26 @@ export const TradingBox = ({ activeGroup, side = "long" }: TradingBoxProps) => { )} - {connected && tradeState === "short" && activeGroup?.pool.quoteTokens[0].userInfo.tokenAccount.balance === 0 && ( -
- -
-

- You need to hold {activeGroup?.pool.quoteTokens[0].meta.tokenSymbol} to open a short position.{" "} - -

+ {connected && + tradeState === "short" && + activeGroup?.pool.quoteTokens[0].userInfo.tokenAccount.balance === 0 && ( +
+ +
+

+ You need to hold {activeGroup?.pool.quoteTokens[0].meta.tokenSymbol} to open a short position.{" "} + +

+
-
- )} + )} {isActiveWithCollat ? ( <>
diff --git a/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/TradingBoxSettings.tsx b/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/TradingBoxSettings.tsx index 6f31d1b87a..995286e5ee 100644 --- a/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/TradingBoxSettings.tsx +++ b/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/TradingBoxSettings.tsx @@ -3,7 +3,7 @@ import React from "react"; import { IconArrowLeft } from "@tabler/icons-react"; import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group"; -import { Slippage, PriorityFees } from "./components"; +import { Slippage } from "./components"; type TradingBoxSettingsProps = { toggleSettings: (mode: boolean) => void; @@ -30,7 +30,7 @@ export const TradingBoxSettings = ({ toggleSettings, slippageBps, setSlippageBps -
+ {/*
Slippage @@ -40,12 +40,11 @@ export const TradingBoxSettings = ({ toggleSettings, slippageBps, setSlippageBps Priority Fee -
+
*/}
{settingsMode === SettingsState.Slippage && ( )} - {settingsMode === SettingsState.PriorityFee && }
); diff --git a/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/components/PriorityFees.tsx b/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/components/PriorityFees.tsx deleted file mode 100644 index e47cbcb363..0000000000 --- a/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/components/PriorityFees.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from "react"; - -import { IconInfoCircle } from "@tabler/icons-react"; -import { cn } from "@mrgnlabs/mrgn-utils"; - -import { useUiStore } from "~/store"; - -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; -import { Button } from "~/components/ui/button"; -import { Input } from "~/components/ui/input"; - -type PriorityFeesProps = { - toggleSettings: (mode: boolean) => void; -}; - -const priorityFeeOptions = [ - { - label: "Normal", - value: 0, - }, - { - label: "High", - value: 0.00005, - }, - { - label: "Mamas", - value: 0.005, - }, -]; - -export const PriorityFees = ({ toggleSettings }: PriorityFeesProps) => { - const [priorityFee, setPriorityFee] = useUiStore((state) => [state.priorityFee, state.setPriorityFee]); - const [selectedPriorityFee, setSelectedPriorityFee] = React.useState(priorityFee); - - const priorityFeeRef = React.useRef(null); - const [isCustomPriorityFeeMode, setIsCustomPriorityFeeMode] = React.useState(false); - const [customPriorityFee, setCustomPriorityFee] = React.useState(null); - - return ( -
-

- Set transaction priority{" "} - - - - - - -
-

Priority fees are paid to the Solana network.

-

This additional fee helps boost how a transaction is prioritized.

-
-
-
-
-

-
    - {priorityFeeOptions.map((option) => ( -
  • - -
  • - ))} -
-

or set manually

-
- setIsCustomPriorityFeeMode(true)} - onChange={() => setCustomPriorityFee(parseFloat(priorityFeeRef.current?.value || "0"))} - /> - SOL -
- -
- ); -}; diff --git a/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/components/index.ts b/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/components/index.ts index d14340f1a8..f2513227a3 100644 --- a/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/components/index.ts +++ b/apps/marginfi-v2-trading/src/components/common/TradingBox/components/TradingBoxSettings/components/index.ts @@ -1,2 +1 @@ -export * from "./PriorityFees"; export * from "./Slippage"; diff --git a/apps/marginfi-v2-trading/src/components/common/TradingBox/tradingBox.utils.tsx b/apps/marginfi-v2-trading/src/components/common/TradingBox/tradingBox.utils.tsx index 7e9f69e6ae..38694707c9 100644 --- a/apps/marginfi-v2-trading/src/components/common/TradingBox/tradingBox.utils.tsx +++ b/apps/marginfi-v2-trading/src/components/common/TradingBox/tradingBox.utils.tsx @@ -10,7 +10,13 @@ import { SimulationResult, } from "@mrgnlabs/marginfi-client-v2"; import { AccountSummary, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; -import { Wallet, percentFormatter, tokenPriceFormatter, usdFormatter } from "@mrgnlabs/mrgn-common"; +import { + TransactionBroadcastType, + Wallet, + percentFormatter, + tokenPriceFormatter, + usdFormatter, +} from "@mrgnlabs/mrgn-common"; import { ActionMethod, DYNAMIC_SIMULATION_ERRORS, @@ -19,10 +25,8 @@ import { LoopingOptions, showErrorToast, MultiStepToastHandle, - STATIC_SIMULATION_ERRORS, cn, extractErrorString, - isBankOracleStale, } from "@mrgnlabs/mrgn-utils"; import { IconPyth, IconSwitchboard } from "~/components/ui/icons"; @@ -37,32 +41,37 @@ export async function looping({ depositAmount, options, priorityFee, - isTxnSplit = false, + broadcastType, }: { marginfiClient: MarginfiClient | null; marginfiAccount: MarginfiAccountWrapper; bank: ExtendedBankInfo; depositAmount: number; options: LoopingOptions; - priorityFee?: number; - isTxnSplit?: boolean; + priorityFee: number; + broadcastType: TransactionBroadcastType; }) { if (marginfiClient === null) { showErrorToast({ message: "Marginfi client not ready" }); return; } - const multiStepToast = new MultiStepToastHandle( - "Looping", - [{ label: `Executing looping ${bank.meta.tokenSymbol} with ${options.loopingBank.meta.tokenSymbol}` }] - ); + const multiStepToast = new MultiStepToastHandle("Looping", [ + { label: `Executing looping ${bank.meta.tokenSymbol} with ${options.loopingBank.meta.tokenSymbol}` }, + ]); multiStepToast.start(); try { let sigs: string[] = []; if (options.loopingTxn) { - sigs = await marginfiClient.processTransactions([...options.feedCrankTxs, options.loopingTxn]); + sigs = await marginfiClient.processTransactions( + [...options.feedCrankTxs, options.loopingTxn], + undefined, + undefined, + broadcastType, + true + ); } else { const { flashloanTx, feedCrankTxs } = await loopingBuilder({ marginfiAccount, @@ -70,9 +79,15 @@ export async function looping({ depositAmount, options, priorityFee, - // isTxnSplit, + broadcastType, }); - sigs = await marginfiClient.processTransactions([...feedCrankTxs, flashloanTx]); + sigs = await marginfiClient.processTransactions( + [...feedCrankTxs, flashloanTx], + undefined, + undefined, + broadcastType, + true + ); } multiStepToast.setSuccessAndNext(); return sigs; diff --git a/apps/marginfi-v2-trading/src/pages/_app.tsx b/apps/marginfi-v2-trading/src/pages/_app.tsx index 67a3732e8d..a53d1867b4 100644 --- a/apps/marginfi-v2-trading/src/pages/_app.tsx +++ b/apps/marginfi-v2-trading/src/pages/_app.tsx @@ -12,10 +12,11 @@ import { TipLinkWalletAutoConnect } from "@tiplink/wallet-adapter-react-ui"; import { ToastContainer } from "react-toastify"; import { Analytics } from "@vercel/analytics/react"; import { BankMetadataRaw } from "@mrgnlabs/mrgn-common"; -import { Desktop, Mobile, init as initAnalytics } from "@mrgnlabs/mrgn-utils"; -import { AuthDialog } from "@mrgnlabs/mrgn-ui"; +import { DEFAULT_MAX_CAP, Desktop, Mobile, init as initAnalytics } from "@mrgnlabs/mrgn-utils"; +import { ActionProvider, AuthDialog } from "@mrgnlabs/mrgn-ui"; import config from "~/config"; +import { useUiStore } from "~/store"; import { TradePovider } from "~/context"; import { WALLET_ADAPTERS } from "~/config/wallets"; import { BANK_METADATA_MAP } from "~/config/trade"; @@ -39,6 +40,13 @@ export default function MrgnApp({ Component, pageProps, path, bank }: AppProps & const { query, isReady } = useRouter(); const [ready, setReady] = React.useState(false); + const [broadcastType, priorityType, maxCap, maxCapType] = useUiStore((state) => [ + state.broadcastType, + state.priorityType, + state.maxCap, + state.maxCapType, + ]); + React.useEffect(() => { setReady(true); initAnalytics(); @@ -52,31 +60,38 @@ export default function MrgnApp({ Component, pageProps, path, bank }: AppProps & - -
-
- - - + + +
+
+ + + +
+ +
+
+ + + + +
-
- - - - - -
- -
-
- - - - -
-
+ + + + + +
+
+
diff --git a/apps/marginfi-v2-trading/src/pages/api/bundles/tip.ts b/apps/marginfi-v2-trading/src/pages/api/bundles/tip.ts new file mode 100644 index 0000000000..1790e42d29 --- /dev/null +++ b/apps/marginfi-v2-trading/src/pages/api/bundles/tip.ts @@ -0,0 +1,48 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +const JITO_API = "http://bundles-api-rest.jito.wtf"; + +export interface TipFloorDataResponse { + time: string; + landed_tips_25th_percentile: number; + landed_tips_50th_percentile: number; + landed_tips_75th_percentile: number; + landed_tips_95th_percentile: number; + landed_tips_99th_percentile: number; + ema_landed_tips_50th_percentile: number; +} + +/* + Get jito tip data for at least 50 percentile result +*/ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // use abort controller to restrict fetch to 5 seconds + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + }, 5000); + + // Fetch from API and update cache + try { + const response = await fetch(`${JITO_API}/api/v1/bundles/tip_floor`, { + headers: { + Accept: "application/json", + }, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + const data: TipFloorDataResponse = (await response.json())[0]; + + // cache for 4 minutes + res.setHeader("Cache-Control", "s-maxage=240, stale-while-revalidate=59"); + res.status(200).json(data); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ error: "Error fetching data" }); + } +} diff --git a/apps/marginfi-v2-trading/src/store/actionBoxStore.ts b/apps/marginfi-v2-trading/src/store/actionBoxStore.ts index e5c928e725..17686b426e 100644 --- a/apps/marginfi-v2-trading/src/store/actionBoxStore.ts +++ b/apps/marginfi-v2-trading/src/store/actionBoxStore.ts @@ -70,7 +70,7 @@ interface ActionBoxState { slippageBps: number, connection: Connection, priorityFee: number, - platformFeeBps?: number + platformFeeBps: number ) => void; setIsLoading: (isLoading: boolean) => void; } @@ -226,7 +226,8 @@ const stateCreator: StateCreator = (set, get) => ({ slippageBps, connection, priorityFee, - platformFeeBps + platformFeeBps, + "BUNDLE" ); if (repayCollat && "repayTxn" in repayCollat) { diff --git a/apps/marginfi-v2-trading/src/store/uiStore.ts b/apps/marginfi-v2-trading/src/store/uiStore.ts index be61051ce0..0a0a3a4a8e 100644 --- a/apps/marginfi-v2-trading/src/store/uiStore.ts +++ b/apps/marginfi-v2-trading/src/store/uiStore.ts @@ -4,6 +4,13 @@ import { persist } from "zustand/middleware"; import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; import { LendingModes, PreviousTxn } from "~/types"; +import { + MaxCapType, + TransactionBroadcastType, + TransactionPriorityType, + TransactionSettings, +} from "@mrgnlabs/mrgn-common"; +import { DEFAULT_PRIORITY_SETTINGS } from "@mrgnlabs/mrgn-utils"; export enum WalletState { DEFAULT = "default", @@ -20,7 +27,6 @@ interface UiState { isWalletAuthDialogOpen: boolean; isWalletOpen: boolean; lendingMode: LendingModes; - priorityFee: number; slippageBps: number; platformFeeBps: number; isActionComplete: boolean; @@ -28,18 +34,22 @@ interface UiState { isActionBoxInputFocussed: boolean; walletState: WalletState; isOnrampActive: boolean; + broadcastType: TransactionBroadcastType; + priorityType: TransactionPriorityType; + maxCapType: MaxCapType; + maxCap: number; // Actions setIsWalletAuthDialogOpen: (isOpen: boolean) => void; setIsWalletOpen: (isOpen: boolean) => void; setLendingMode: (lendingMode: LendingModes) => void; - setPriorityFee: (priorityFee: number) => void; setSlippageBps: (slippageBps: number) => void; setIsActionComplete: (isActionSuccess: boolean) => void; setPreviousTxn: (previousTxn: PreviousTxn) => void; setWalletState: (walletState: WalletState) => void; setIsActionBoxInputFocussed: (isFocussed: boolean) => void; setIsOnrampActive: (isOnrampActive: boolean) => void; + setTransactionSettings: (settings: TransactionSettings) => void; } function createUiStore() { @@ -47,11 +57,6 @@ function createUiStore() { persist(stateCreator, { name: "uiStore", onRehydrateStorage: () => (state) => { - // overwrite priority fee - if (process.env.NEXT_PUBLIC_INIT_PRIO_FEE && process.env.NEXT_PUBLIC_INIT_PRIO_FEE !== "0") { - state?.setPriorityFee(Number(process.env.NEXT_PUBLIC_INIT_PRIO_FEE)); - } - // overwrite wallet on mobile // covers private key export modal when open if (window.innerWidth < 768) { @@ -71,12 +76,12 @@ const stateCreator: StateCreator = (set, get) => ({ actionMode: ActionType.Deposit, platformFeeBps: 30, selectedTokenBank: null, - priorityFee: 0, isActionComplete: false, previousTxn: null, isActionBoxInputFocussed: false, walletState: WalletState.DEFAULT, isOnrampActive: false, + ...DEFAULT_PRIORITY_SETTINGS, // Actions setIsWalletAuthDialogOpen: (isOpen: boolean) => set({ isWalletAuthDialogOpen: isOpen }), @@ -85,13 +90,13 @@ const stateCreator: StateCreator = (set, get) => ({ set({ lendingMode: lendingMode, }), - setPriorityFee: (priorityFee: number) => set({ priorityFee: priorityFee }), setSlippageBps: (slippageBps: number) => set({ slippageBps: slippageBps }), setIsActionComplete: (isActionComplete: boolean) => set({ isActionComplete: isActionComplete }), setPreviousTxn: (previousTxn: PreviousTxn) => set({ previousTxn: previousTxn }), setWalletState: (walletState: WalletState) => set({ walletState: walletState }), setIsActionBoxInputFocussed: (isFocussed: boolean) => set({ isActionBoxInputFocussed: isFocussed }), setIsOnrampActive: (isOnrampActive: boolean) => set({ isOnrampActive: isOnrampActive }), + setTransactionSettings: (settings: TransactionSettings) => set({ ...settings }), }); export { createUiStore }; diff --git a/apps/marginfi-v2-trading/src/utils/tradingActions.ts b/apps/marginfi-v2-trading/src/utils/tradingActions.ts index 7126a31bc7..ec1e8047d9 100644 --- a/apps/marginfi-v2-trading/src/utils/tradingActions.ts +++ b/apps/marginfi-v2-trading/src/utils/tradingActions.ts @@ -30,6 +30,7 @@ import { ExtendedBankInfo, clearAccountCache, ActiveBankInfo } from "@mrgnlabs/m import { TradeSide } from "~/components/common/TradingBox/tradingBox.utils"; import { WalletContextStateOverride } from "~/components/wallet-v2/hooks/use-wallet.hook"; +import { TransactionBroadcastType } from "@mrgnlabs/mrgn-common"; export async function createMarginfiGroup({ marginfiClient, @@ -163,6 +164,7 @@ export async function executeLeverageAction({ loopingObject: _loopingObject, priorityFee, slippageBps, + broadcastType, }: { marginfiClient: MarginfiClient | null; marginfiAccount: MarginfiAccountWrapper | null; @@ -175,6 +177,7 @@ export async function executeLeverageAction({ loopingObject: LoopingObject | null; priorityFee: number; slippageBps: number; + broadcastType: TransactionBroadcastType; }) { if (marginfiClient === null) { showErrorToast("Marginfi client not ready"); @@ -237,6 +240,7 @@ export async function executeLeverageAction({ loopObject: loopingObject, priorityFee, isTrading: true, + broadcastType: broadcastType, }); if ("loopingTxn" in result) { @@ -261,7 +265,13 @@ export async function executeLeverageAction({ let txnSig: string[] = []; if (loopingObject.feedCrankTxs) { - txnSig = await marginfiClient.processTransactions([...loopingObject.feedCrankTxs, loopingObject.loopingTxn]); + txnSig = await marginfiClient.processTransactions( + [...loopingObject.feedCrankTxs, loopingObject.loopingTxn], + undefined, + undefined, + broadcastType, + true + ); } else { txnSig = [await marginfiClient.processTransaction(loopingObject.loopingTxn)]; } diff --git a/apps/marginfi-v2-ui/src/components/common/Navbar/Navbar.tsx b/apps/marginfi-v2-ui/src/components/common/Navbar/Navbar.tsx index 475cc3cb2b..e2eb24c698 100644 --- a/apps/marginfi-v2-ui/src/components/common/Navbar/Navbar.tsx +++ b/apps/marginfi-v2-ui/src/components/common/Navbar/Navbar.tsx @@ -5,11 +5,10 @@ import Image from "next/image"; import { useRouter } from "next/router"; import { PublicKey } from "@solana/web3.js"; -// import LipAccount from "@mrgnlabs/lip-client/src/account"; -import { IconBell, IconBrandTelegram } from "@tabler/icons-react"; +import { IconBell, IconBrandTelegram, IconSettings } from "@tabler/icons-react"; import { collectRewardsBatch, capture, cn } from "@mrgnlabs/mrgn-utils"; -import { Wallet } from "@mrgnlabs/mrgn-ui"; +import { Settings, Wallet } from "@mrgnlabs/mrgn-ui"; import { useMrgnlendStore, useUiStore, useUserProfileStore } from "~/store"; import { useFirebaseAccount } from "~/hooks/useFirebaseAccount"; @@ -52,7 +51,16 @@ export const Navbar: FC = () => { state.fetchMrgnlendState, ]); - const [isOraclesStale, priorityFee] = useUiStore((state) => [state.isOraclesStale, state.priorityFee]); + const { isOraclesStale, priorityType, broadcastType, maxCap, maxCapType, setTransactionSettings } = useUiStore( + (state) => ({ + isOraclesStale: state.isOraclesStale, + priorityType: state.priorityType, + broadcastType: state.broadcastType, + maxCap: state.maxCap, + maxCapType: state.maxCapType, + setTransactionSettings: state.setTransactionSettings, + }) + ); const [userPointsData] = useUserProfileStore((state) => [state.userPointsData]); @@ -154,6 +162,7 @@ export const Navbar: FC = () => { }`} onClick={async () => { if (!wallet || !selectedAccount || bankAddressesWithEmissions.length === 0) return; + const priorityFee = 0; // code has been removed on new collect rewards so temporary placeholder await collectRewardsBatch(selectedAccount, bankAddressesWithEmissions, priorityFee); }} > @@ -203,6 +212,23 @@ export const Navbar: FC = () => { + + + + + + + + + [state.setIsFetchingData, state.isOraclesStale]); + const [priorityType, broadcastType, maxCapType, maxCap, setIsFetchingData, isOraclesStale] = useUiStore((state) => [ + state.priorityType, + state.broadcastType, + state.maxCapType, + state.maxCap, + state.setIsFetchingData, + state.isOraclesStale, + ]); const [ isMrgnlendStoreInitialized, isRefreshingMrgnlendStore, @@ -105,47 +109,57 @@ export default function MrgnApp({ Component, pageProps, path }: AppProps & MrgnA - - - - - - + + + + + + +
+ +
+
+ + + +
-
- - - - -
- -
- -
- - - - - - + + + + + + + + + diff --git a/apps/marginfi-v2-ui/src/pages/api/bundles/tip.ts b/apps/marginfi-v2-ui/src/pages/api/bundles/tip.ts new file mode 100644 index 0000000000..1790e42d29 --- /dev/null +++ b/apps/marginfi-v2-ui/src/pages/api/bundles/tip.ts @@ -0,0 +1,48 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +const JITO_API = "http://bundles-api-rest.jito.wtf"; + +export interface TipFloorDataResponse { + time: string; + landed_tips_25th_percentile: number; + landed_tips_50th_percentile: number; + landed_tips_75th_percentile: number; + landed_tips_95th_percentile: number; + landed_tips_99th_percentile: number; + ema_landed_tips_50th_percentile: number; +} + +/* + Get jito tip data for at least 50 percentile result +*/ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // use abort controller to restrict fetch to 5 seconds + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + }, 5000); + + // Fetch from API and update cache + try { + const response = await fetch(`${JITO_API}/api/v1/bundles/tip_floor`, { + headers: { + Accept: "application/json", + }, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + const data: TipFloorDataResponse = (await response.json())[0]; + + // cache for 4 minutes + res.setHeader("Cache-Control", "s-maxage=240, stale-while-revalidate=59"); + res.status(200).json(data); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ error: "Error fetching data" }); + } +} diff --git a/apps/marginfi-v2-ui/src/pages/api/oracle/price.ts b/apps/marginfi-v2-ui/src/pages/api/oracle/price.ts index 1c72dcff11..d4406da3aa 100644 --- a/apps/marginfi-v2-ui/src/pages/api/oracle/price.ts +++ b/apps/marginfi-v2-ui/src/pages/api/oracle/price.ts @@ -153,7 +153,7 @@ async function fetchCrossbarPrices(feedHashes: string[]): Promise { controller.abort(); - }, 5000); + }, 8000); try { const feedHashesString = feedHashes.join(","); diff --git a/apps/marginfi-v2-ui/src/store/uiStore.ts b/apps/marginfi-v2-ui/src/store/uiStore.ts index d2f078f6c8..e3518e272a 100644 --- a/apps/marginfi-v2-ui/src/store/uiStore.ts +++ b/apps/marginfi-v2-ui/src/store/uiStore.ts @@ -1,10 +1,15 @@ import { create, StateCreator } from "zustand"; import { persist } from "zustand/middleware"; -import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; -import { LendingModes, PoolTypes } from "@mrgnlabs/mrgn-utils"; +import { + MaxCapType, + TransactionBroadcastType, + TransactionPriorityType, + TransactionSettings, +} from "@mrgnlabs/mrgn-common"; +import { LendingModes, PoolTypes, DEFAULT_PRIORITY_SETTINGS } from "@mrgnlabs/mrgn-utils"; -import { SortType, sortDirection, SortAssetOption, PreviousTxn } from "~/types"; +import { SortType, sortDirection, SortAssetOption } from "~/types"; const SORT_OPTIONS_MAP: { [key in SortType]: SortAssetOption } = { APY_DESC: { @@ -44,8 +49,11 @@ interface UiState { lendingMode: LendingModes; poolFilter: PoolTypes; sortOption: SortAssetOption; - priorityFee: number; assetListSearch: string; + broadcastType: TransactionBroadcastType; + priorityType: TransactionPriorityType; + maxCap: number; + maxCapType: MaxCapType; // Actions setIsMenuDrawerOpen: (isOpen: boolean) => void; @@ -55,20 +63,15 @@ interface UiState { setLendingMode: (lendingMode: LendingModes) => void; setPoolFilter: (poolType: PoolTypes) => void; setSortOption: (sortOption: SortAssetOption) => void; - setPriorityFee: (priorityFee: number) => void; setAssetListSearch: (search: string) => void; + setTransactionSettings: (settings: TransactionSettings) => void; } function createUiStore() { return create()( persist(stateCreator, { name: "uiStore", - onRehydrateStorage: () => (state) => { - // overwrite priority fee - if (process.env.NEXT_PUBLIC_INIT_PRIO_FEE && process.env.NEXT_PUBLIC_INIT_PRIO_FEE !== "0") { - state?.setPriorityFee(Number(process.env.NEXT_PUBLIC_INIT_PRIO_FEE)); - } - }, + onRehydrateStorage: () => (state) => {}, }) ); } @@ -82,8 +85,8 @@ const stateCreator: StateCreator = (set, get) => ({ lendingMode: LendingModes.LEND, poolFilter: PoolTypes.ALL, sortOption: SORT_OPTIONS_MAP[SortType.TVL_DESC], - priorityFee: 0, assetListSearch: "", + ...DEFAULT_PRIORITY_SETTINGS, // Actions setIsMenuDrawerOpen: (isOpen: boolean) => set({ isMenuDrawerOpen: isOpen }), @@ -97,8 +100,8 @@ const stateCreator: StateCreator = (set, get) => ({ setIsOraclesStale: (isOraclesStale: boolean) => set({ isOraclesStale: isOraclesStale }), setPoolFilter: (poolType: PoolTypes) => set({ poolFilter: poolType }), setSortOption: (sortOption: SortAssetOption) => set({ sortOption: sortOption }), - setPriorityFee: (priorityFee: number) => set({ priorityFee: priorityFee }), setAssetListSearch: (search: string) => set({ assetListSearch: search }), + setTransactionSettings: (settings: TransactionSettings) => set({ ...settings }), }); export { createUiStore, SORT_OPTIONS_MAP }; diff --git a/packages/marginfi-client-v2/src/client.ts b/packages/marginfi-client-v2/src/client.ts index 47a422d601..ef366db9bc 100644 --- a/packages/marginfi-client-v2/src/client.ts +++ b/packages/marginfi-client-v2/src/client.ts @@ -30,6 +30,7 @@ import { NodeWallet, simulateBundle, sleep, + TransactionBroadcastType, TransactionOptions, Wallet, } from "@mrgnlabs/mrgn-common"; @@ -47,6 +48,7 @@ import { BankConfigOpt, BankConfig, makeBundleTipIx, + makeTxPriorityIx, } from "."; import { MarginfiAccountWrapper } from "./models/account/wrapper"; import { @@ -607,20 +609,23 @@ class MarginfiClient { */ async createMarginfiAccount( opts?: TransactionOptions, - createOpts?: { newAccountKey?: PublicKey | undefined } + createOpts?: { newAccountKey?: PublicKey | undefined }, + priorityFeeUi?: number, + broadcastType?: TransactionBroadcastType ): Promise { const dbg = require("debug")("mfi:client"); const accountKeypair = Keypair.generate(); const newAccountKey = createOpts?.newAccountKey ?? accountKeypair.publicKey; - const bundleTipIx = makeBundleTipIx(this.provider.publicKey); + const { bundleTipIx, priorityFeeIx } = makeTxPriorityIx(this.provider.publicKey, priorityFeeUi, broadcastType); + 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(bundleTipIx, ...ixs.instructions); + const tx = new Transaction().add(priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...ixs.instructions); const sig = await this.processTransaction(tx, signers, opts); dbg("Created Marginfi account %s", sig); @@ -765,7 +770,9 @@ class MarginfiClient { async processTransactions( transactions: (VersionedTransaction | Transaction)[], signers?: Array, - opts?: TransactionOptions + opts?: TransactionOptions, + broadcastType: TransactionBroadcastType = "BUNDLE", + isSequentialTxs: boolean = true ): Promise { let signatures: TransactionSignature[] = [""]; @@ -868,10 +875,34 @@ class MarginfiClient { }); } - signatures = await this.sendTransactionAsBundle(base58Txs).catch( - async () => + let sendTxsRpc = async (versionedTransaction: VersionedTransaction[]): Promise => []; + + if (isSequentialTxs) { + sendTxsRpc = async (txs: VersionedTransaction[]) => { + let sigs = []; + for (const tx of txs) { + const signature = await connection.sendTransaction(tx, { + // minContextSlot: mergedOpts.minContextSlot, + skipPreflight: mergedOpts.skipPreflight, + preflightCommitment: mergedOpts.preflightCommitment, + maxRetries: mergedOpts.maxRetries, + }); + await connection.confirmTransaction( + { + blockhash, + lastValidBlockHeight, + signature, + }, + "confirmed" + ); + sigs.push(signature); + } + return sigs; + }; + } else { + sendTxsRpc = async (txs: VersionedTransaction[]) => await Promise.all( - versionedTransactions.map(async (versionedTransaction) => { + txs.map(async (versionedTransaction) => { const signature = await connection.sendTransaction(versionedTransaction, { // minContextSlot: mergedOpts.minContextSlot, skipPreflight: mergedOpts.skipPreflight, @@ -880,8 +911,16 @@ class MarginfiClient { }); return signature; }) - ) - ); + ); + } + + if (broadcastType === "BUNDLE") { + signatures = await this.sendTransactionAsBundle(base58Txs).catch( + async () => await sendTxsRpc(versionedTransactions) + ); + } else { + signatures = await sendTxsRpc(versionedTransactions); + } await Promise.all( signatures.map(async (signature) => { diff --git a/packages/marginfi-client-v2/src/models/account/wrapper.ts b/packages/marginfi-client-v2/src/models/account/wrapper.ts index cd231053af..c4811e21b8 100644 --- a/packages/marginfi-client-v2/src/models/account/wrapper.ts +++ b/packages/marginfi-client-v2/src/models/account/wrapper.ts @@ -7,6 +7,8 @@ import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, shortenAddress, + TransactionPriorityType, + TransactionBroadcastType, } from "@mrgnlabs/mrgn-common"; import * as sb from "@switchboard-xyz/on-demand"; import { Address, BorshCoder, Idl, translateAddress } from "@coral-xyz/anchor"; @@ -28,6 +30,7 @@ import BigNumber from "bignumber.js"; import { MakeBorrowIxOpts, MakeDepositIxOpts, + makePriorityFeeIx, MakeRepayIxOpts, MakeWithdrawIxOpts, MarginfiClient, @@ -291,31 +294,6 @@ class MarginfiAccountWrapper { return computeLoopingParams(principal, targetLeverage, depositBank, borrowBank, depositPriceInfo, borrowPriceInfo); } - makePriorityFeeIx(priorityFeeUi?: number): TransactionInstruction[] { - const priorityFeeIx: TransactionInstruction[] = []; - const limitCU = 1_400_000; - - let microLamports: number = 1; - - if (priorityFeeUi) { - // if priority fee is above 0.2 SOL discard it for safety reasons - const isAbsurdPriorityFee = priorityFeeUi > 0.2; - - if (!isAbsurdPriorityFee) { - const priorityFeeMicroLamports = priorityFeeUi * LAMPORTS_PER_SOL * 1_000_000; - microLamports = Math.round(priorityFeeMicroLamports / limitCU); - } - } - - priorityFeeIx.push( - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports, - }) - ); - - return priorityFeeIx; - } - makeComputeBudgetIx(): TransactionInstruction[] { // Add additional CU request if necessary let cuRequestIxs: TransactionInstruction[] = []; @@ -387,7 +365,8 @@ class MarginfiAccountWrapper { repayAll: boolean = false, swapIxs: TransactionInstruction[], swapLookupTables: AddressLookupTableAccount[], - priorityFeeUi?: number + priorityFeeUi?: number, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise { const debug = require("debug")(`mfi:margin-account:${this.address.toString()}:repay`); debug("Repaying %s into marginfi account (bank: %s), repay all: %s", repayAmount, borrowBankAddress, repayAll); @@ -404,8 +383,12 @@ class MarginfiAccountWrapper { priorityFeeUi ); - // assumes only one tx - const sigs = await this.client.processTransactions([...feedCrankTxs, flashloanTx]); + const sigs = await this.client.processTransactions( + [...feedCrankTxs, flashloanTx], + undefined, + undefined, + broadcastType + ); debug("Repay with collateral successful %s", sigs.pop() ?? ""); return sigs; @@ -476,7 +459,8 @@ class MarginfiAccountWrapper { swapLookupTables: AddressLookupTableAccount[], priorityFeeUi?: number, isTxnSplitParam?: boolean, - blockhashArg?: string + blockhashArg?: string, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise<{ flashloanTx: VersionedTransaction; feedCrankTxs: VersionedTransaction[]; @@ -486,7 +470,11 @@ class MarginfiAccountWrapper { blockhashArg ?? (await this._program.provider.connection.getLatestBlockhash("confirmed")).blockhash; const setupIxs = await this.makeSetupIx([borrowBankAddress, depositBankAddress]); const cuRequestIxs = this.makeComputeBudgetIx(); - const priorityFeeIx = this.makePriorityFeeIx(priorityFeeUi); + const { bundleTipIx, priorityFeeIx } = makeTxPriorityIx( + this.client.provider.publicKey, + priorityFeeUi, + broadcastType + ); const withdrawIxs = await this.makeWithdrawIx(withdrawAmount, depositBankAddress, withdrawAll, { createAtas: false, wrapAndUnwrapSol: false, @@ -495,7 +483,6 @@ class MarginfiAccountWrapper { createAtas: false, wrapAndUnwrapSol: false, }); - const bundleTipIx = makeBundleTipIx(this.client.provider.publicKey); const lookupTables = this.client.addressLookupTables; const { instructions: updateFeedIxs, luts: feedLuts } = await this.makeUpdateFeedIx([ @@ -507,23 +494,18 @@ class MarginfiAccountWrapper { let feedCrankTxs: VersionedTransaction[] = []; - // isTxnSplit forced set to true as we're always splitting now - const isTxnSplit = true; //isTxnSplitParam - if (isTxnSplit) { - const message = new TransactionMessage({ - payerKey: this.client.wallet.publicKey, - recentBlockhash: blockhash, - instructions: [bundleTipIx, ...updateFeedIxs], - }).compileToV0Message([...addressLookupTableAccounts, ...feedLuts]); + const message = new TransactionMessage({ + payerKey: this.client.wallet.publicKey, + recentBlockhash: blockhash, + instructions: [priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...updateFeedIxs], + }).compileToV0Message([...addressLookupTableAccounts, ...feedLuts]); - feedCrankTxs = [new VersionedTransaction(message)]; - } + feedCrankTxs = [new VersionedTransaction(message)]; const flashloanTx = await this.buildFlashLoanTx({ ixs: [ - ...priorityFeeIx, + ...[priorityFeeIx], ...cuRequestIxs, - ...(isTxnSplit ? [] : [bundleTipIx]), ...setupIxs, ...withdrawIxs.instructions, ...swapIxs, @@ -640,8 +622,8 @@ class MarginfiAccountWrapper { swapIxs: TransactionInstruction[], swapLookupTables: AddressLookupTableAccount[], priorityFeeUi?: number, - createAtas?: boolean - // isTxnSplitParam?: boolean + createAtas?: boolean, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise<{ flashloanTx: VersionedTransaction; feedCrankTxs: VersionedTransaction[]; @@ -654,7 +636,12 @@ class MarginfiAccountWrapper { const setupIxs = createAtas ? await this.makeSetupIx([depositBankAddress, borrowBankAddress]) : []; const cuRequestIxs = this.makeComputeBudgetIx(); - const priorityFeeIx = this.makePriorityFeeIx(priorityFeeUi); + + const { bundleTipIx, priorityFeeIx } = makeTxPriorityIx( + this.client.provider.publicKey, + priorityFeeUi, + broadcastType + ); const borrowIxs = await this.makeBorrowIx(borrowAmount, borrowBankAddress, { createAtas: true, wrapAndUnwrapSol: false, @@ -662,7 +649,6 @@ class MarginfiAccountWrapper { const depositIxs = await this.makeDepositIx(depositAmount, depositBankAddress, { wrapAndUnwrapSol: true, }); - const bundleTipIx = makeBundleTipIx(this.client.provider.publicKey); const clientLookupTables = this.client.addressLookupTables; const { instructions: updateFeedIxs, luts: feedLuts } = await this.makeUpdateFeedIx([ @@ -675,13 +661,13 @@ class MarginfiAccountWrapper { const message = new TransactionMessage({ payerKey: this.client.wallet.publicKey, recentBlockhash: blockhash, - instructions: [bundleTipIx, ...updateFeedIxs, ...setupIxs], + instructions: [priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...updateFeedIxs, ...setupIxs], }).compileToV0Message([...clientLookupTables, ...feedLuts]); const feedCrankTxs = [new VersionedTransaction(message)]; const flashloanTx = await this.buildFlashLoanTx({ - ixs: [...priorityFeeIx, ...cuRequestIxs, ...borrowIxs.instructions, ...swapIxs, ...depositIxs.instructions], + ixs: [...[priorityFeeIx], ...cuRequestIxs, ...borrowIxs.instructions, ...swapIxs, ...depositIxs.instructions], addressLookupTableAccounts: [...clientLookupTables, ...swapLookupTables], }); @@ -707,22 +693,35 @@ class MarginfiAccountWrapper { ); } - async deposit(amount: Amount, bankAddress: PublicKey, opt: MakeDepositIxOpts = {}): Promise { + async deposit( + amount: Amount, + bankAddress: PublicKey, + opt: MakeDepositIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" + ): Promise { const debug = require("debug")(`mfi:margin-account:${this.address.toString()}:deposit`); debug("Depositing %s into marginfi account (bank: %s)", amount, shortenAddress(bankAddress)); - const tx = await this.makeDepositTx(amount, bankAddress, opt); + const tx = await this.makeDepositTx(amount, bankAddress, opt, broadcastType); const sig = await this.client.processTransaction(tx, []); debug("Depositing successful %s", sig); return sig; } - async makeDepositTx(amount: Amount, bankAddress: PublicKey, opt: MakeDepositIxOpts = {}): Promise { - const priorityFeeIx = this.makePriorityFeeIx(opt.priorityFeeUi); - const bundleTipIx = makeBundleTipIx(this.client.provider.publicKey); + async makeDepositTx( + amount: Amount, + bankAddress: PublicKey, + opt: MakeDepositIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" + ): Promise { + const { bundleTipIx, priorityFeeIx } = makeTxPriorityIx( + this.client.provider.publicKey, + opt.priorityFeeUi, + broadcastType + ); const ixs = await this.makeDepositIx(amount, bankAddress, opt); - const tx = new Transaction().add(bundleTipIx, ...priorityFeeIx, ...ixs.instructions); + const tx = new Transaction().add(priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...ixs.instructions); return tx; } @@ -794,12 +793,13 @@ class MarginfiAccountWrapper { amount: Amount, bankAddress: PublicKey, repayAll: boolean = false, - opt: MakeRepayIxOpts = {} + opt: MakeRepayIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise { const debug = require("debug")(`mfi:margin-account:${this.address.toString()}:repay`); debug("Repaying %s into marginfi account (bank: %s), repay all: %s", amount, bankAddress, repayAll); - const tx = await this.makeRepayTx(amount, bankAddress, repayAll, opt); + const tx = await this.makeRepayTx(amount, bankAddress, repayAll, opt, broadcastType); const sig = await this.client.processTransaction(tx, []); debug("Depositing successful %s", sig); @@ -811,12 +811,17 @@ class MarginfiAccountWrapper { amount: Amount, bankAddress: PublicKey, repayAll: boolean = false, - opt: MakeRepayIxOpts = {} + opt: MakeRepayIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise { - const priorityFeeIx = this.makePriorityFeeIx(opt.priorityFeeUi); - const bundleTipIx = makeBundleTipIx(this.client.provider.publicKey); + const { priorityFeeIx, bundleTipIx } = makeTxPriorityIx( + this.client.provider.publicKey, + opt.priorityFeeUi, + broadcastType + ); + const ixs = await this.makeRepayIx(amount, bankAddress, repayAll, opt); - const tx = new Transaction().add(bundleTipIx, ...priorityFeeIx, ...ixs.instructions); + const tx = new Transaction().add(priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...ixs.instructions); return tx; } @@ -855,17 +860,22 @@ class MarginfiAccountWrapper { amount: Amount; bankAddress: PublicKey; }[], - opt: MakeWithdrawIxOpts = {} + opt: MakeWithdrawIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise { const debug = require("debug")(`mfi:margin-account:${this.address.toString()}:withdraw`); debug("Withdrawing all from marginfi account"); - const priorityFeeIx = this.makePriorityFeeIx(opt.priorityFeeUi); + const { priorityFeeIx, bundleTipIx } = makeTxPriorityIx( + this.client.provider.publicKey, + opt.priorityFeeUi, + broadcastType + ); const cuRequestIxs = this.makeComputeBudgetIx(); let ixs = []; for (const bank of banks) { ixs.push(...(await this.makeWithdrawIx(bank.amount, bank.bankAddress, true, opt)).instructions); } - const tx = new Transaction().add(...priorityFeeIx, ...cuRequestIxs, ...ixs); + const tx = new Transaction().add(...cuRequestIxs, priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...ixs); return tx; } @@ -873,15 +883,27 @@ class MarginfiAccountWrapper { amount: Amount, bankAddress: PublicKey, withdrawAll: boolean = false, - opt: MakeWithdrawIxOpts = {} + opt: MakeWithdrawIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise { const debug = require("debug")(`mfi:margin-account:${this.address.toString()}:withdraw`); debug("Withdrawing %s from marginfi account", amount); - const { feedCrankTxs, withdrawTx } = await this.makeWithdrawTx(amount, bankAddress, withdrawAll, opt); + const { feedCrankTxs, withdrawTx } = await this.makeWithdrawTx( + amount, + bankAddress, + withdrawAll, + opt, + broadcastType + ); // process multiple transactions if feed updates required - const sigs = await this.client.processTransactions([...feedCrankTxs, withdrawTx]); + const sigs = await this.client.processTransactions( + [...feedCrankTxs, withdrawTx], + undefined, + undefined, + broadcastType + ); debug("Withdrawing successful %s", sigs.pop()); return sigs; @@ -891,14 +913,18 @@ class MarginfiAccountWrapper { amount: Amount, bankAddress: PublicKey, withdrawAll: boolean = false, - opt: MakeWithdrawIxOpts = {} + opt: MakeWithdrawIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise<{ feedCrankTxs: VersionedTransaction[]; withdrawTx: VersionedTransaction; addressLookupTableAccounts: AddressLookupTableAccount[]; }> { - const bundleTipIx = makeBundleTipIx(this.client.provider.publicKey); - const priorityFeeIxs = this.makePriorityFeeIx(opt.priorityFeeUi); + const { bundleTipIx, priorityFeeIx } = makeTxPriorityIx( + this.client.provider.publicKey, + opt.priorityFeeUi, + broadcastType + ); const cuRequestIxs = this.makeComputeBudgetIx(); const { instructions: updateFeedIxs, luts: feedLuts } = await this.makeUpdateFeedIx([]); const ixs = await this.makeWithdrawIx(amount, bankAddress, withdrawAll, opt); @@ -913,7 +939,7 @@ class MarginfiAccountWrapper { feedCrankTxs.push( new VersionedTransaction( new TransactionMessage({ - instructions: updateFeedIxs, + instructions: [priorityFeeIx, ...updateFeedIxs], payerKey: this.authority, recentBlockhash: blockhash, }).compileToV0Message([...feedLuts]) @@ -923,7 +949,7 @@ class MarginfiAccountWrapper { const withdrawTx = new VersionedTransaction( new TransactionMessage({ - instructions: [bundleTipIx, ...cuRequestIxs, ...priorityFeeIxs, ...ixs.instructions], + instructions: [priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...cuRequestIxs, ...ixs.instructions], payerKey: this.authority, recentBlockhash: blockhash, }).compileToV0Message([...this.client.addressLookupTables]) @@ -956,14 +982,24 @@ class MarginfiAccountWrapper { ); } - async borrow(amount: Amount, bankAddress: PublicKey, opt: MakeBorrowIxOpts = {}): Promise { + async borrow( + amount: Amount, + bankAddress: PublicKey, + opt: MakeBorrowIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" + ): Promise { const debug = require("debug")(`mfi:margin-account:${this.address.toString()}:borrow`); debug("Borrowing %s from marginfi account", amount); const { feedCrankTxs, borrowTx } = await this.makeBorrowTx(amount, bankAddress, opt); // process multiple transactions if feed updates required - const sigs = await this.client.processTransactions([...feedCrankTxs, borrowTx]); + const sigs = await this.client.processTransactions( + [...feedCrankTxs, borrowTx], + undefined, + undefined, + broadcastType + ); debug("Borrowing successful %s", sigs); return sigs; } @@ -971,14 +1007,18 @@ class MarginfiAccountWrapper { async makeBorrowTx( amount: Amount, bankAddress: PublicKey, - opt: MakeBorrowIxOpts = {} + opt: MakeBorrowIxOpts = {}, + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise<{ feedCrankTxs: VersionedTransaction[]; borrowTx: VersionedTransaction; addressLookupTableAccounts: AddressLookupTableAccount[]; }> { - const bundleTipIx = makeBundleTipIx(this.client.provider.publicKey); - const priorityFeeIxs = this.makePriorityFeeIx(opt.priorityFeeUi); + const { bundleTipIx, priorityFeeIx } = makeTxPriorityIx( + this.client.provider.publicKey, + opt.priorityFeeUi, + broadcastType + ); const cuRequestIxs = this.makeComputeBudgetIx(); const { instructions: updateFeedIxs, luts: feedLuts } = await this.makeUpdateFeedIx([bankAddress]); const ixs = await this.makeBorrowIx(amount, bankAddress, opt); @@ -993,7 +1033,7 @@ class MarginfiAccountWrapper { feedCrankTxs.push( new VersionedTransaction( new TransactionMessage({ - instructions: updateFeedIxs, + instructions: [priorityFeeIx, ...updateFeedIxs], payerKey: this.authority, recentBlockhash: blockhash, }).compileToV0Message([...feedLuts]) @@ -1003,7 +1043,7 @@ class MarginfiAccountWrapper { const borrowTx = new VersionedTransaction( new TransactionMessage({ - instructions: [bundleTipIx, ...cuRequestIxs, ...priorityFeeIxs, ...ixs.instructions], + instructions: [...cuRequestIxs, priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...ixs.instructions], payerKey: this.authority, recentBlockhash: blockhash, }).compileToV0Message([...this.client.addressLookupTables]) @@ -1031,11 +1071,18 @@ class MarginfiAccountWrapper { ); } - async withdrawEmissions(bankAddresses: PublicKey[], priorityFeeUi?: number): Promise { + async withdrawEmissions( + bankAddresses: PublicKey[], + priorityFeeUi?: number, + broadcastType: TransactionBroadcastType = "BUNDLE" + ): Promise { const debug = require("debug")(`mfi:margin-account:${this.address.toString()}:withdraw-emissions`); debug("Withdrawing emission from marginfi account (bank: %s)", bankAddresses.map((b) => b.toBase58()).join(", ")); - const bundleTipIx = makeBundleTipIx(this.client.provider.publicKey); - const priorityFeeIx = this.makePriorityFeeIx(priorityFeeUi); + const { bundleTipIx, priorityFeeIx } = makeTxPriorityIx( + this.client.provider.publicKey, + priorityFeeUi, + broadcastType + ); const ixs: TransactionInstruction[] = []; const signers = []; for (const bankAddress of bankAddresses) { @@ -1043,7 +1090,7 @@ class MarginfiAccountWrapper { ixs.push(...ix.instructions); signers.push(ix.keys); } - const tx = new Transaction().add(bundleTipIx, ...priorityFeeIx, ...ixs); + const tx = new Transaction().add(priorityFeeIx, ...(bundleTipIx ? [bundleTipIx] : []), ...ixs); const sig = await this.client.processTransaction(tx, []); debug("Withdrawing emission successful %s", sig); return sig; @@ -1259,7 +1306,27 @@ class MarginfiAccountWrapper { } } -export function makeBundleTipIx(feePayer: PublicKey): TransactionInstruction { +export function makeTxPriorityIx( + feePayer: PublicKey, + priorityFeeUi: number = 0, + broadcastType: TransactionBroadcastType = "BUNDLE" +) { + let bundleTipIx: TransactionInstruction | undefined = undefined; + let priorityFeeIx: TransactionInstruction = makePriorityFeeIx()[0]; + + if (broadcastType === "BUNDLE") { + bundleTipIx = makeBundleTipIx(feePayer, Math.trunc(priorityFeeUi * LAMPORTS_PER_SOL)); + } else { + priorityFeeIx = makePriorityFeeIx(priorityFeeUi)[0]; + } + + return { + bundleTipIx, + priorityFeeIx, + }; +} + +export function makeBundleTipIx(feePayer: PublicKey, bundleTip: number = 100_000): TransactionInstruction { // they have remained constant so function not used (for now) const getTipAccounts = async () => { const response = await fetch("https://mainnet.block-engine.jito.wtf/api/v1/bundles", { @@ -1294,7 +1361,7 @@ export function makeBundleTipIx(feePayer: PublicKey): TransactionInstruction { return SystemProgram.transfer({ fromPubkey: feePayer, toPubkey: new PublicKey(randomTipAccount), - lamports: 100_000, // 100_000 lamports = 0.0001 SOL + lamports: bundleTip, // 100_000 lamports = 0.0001 SOL }); } diff --git a/packages/mrgn-common/src/index.ts b/packages/mrgn-common/src/index.ts index d7ff60f805..5495925ef0 100644 --- a/packages/mrgn-common/src/index.ts +++ b/packages/mrgn-common/src/index.ts @@ -10,4 +10,5 @@ export * from "./formatters"; export * from "./conversion"; export * from "./accounting"; export * from "./spl"; +export * from "./priority"; export { NodeWallet }; diff --git a/packages/mrgn-common/src/priority.ts b/packages/mrgn-common/src/priority.ts new file mode 100644 index 0000000000..fd61f2c7f6 --- /dev/null +++ b/packages/mrgn-common/src/priority.ts @@ -0,0 +1,212 @@ +import type { RecentPrioritizationFees } from "@solana/web3.js"; +import { Connection, type GetRecentPrioritizationFeesConfig } from "@solana/web3.js"; + +export type TransactionBroadcastType = "BUNDLE" | "RPC"; + +export type TransactionPriorityType = "NORMAL" | "HIGH" | "MAMAS"; + +export type MaxCapType = "DYNAMIC" | "MANUAL"; + +export type TransactionSettings = { + broadcastType: TransactionBroadcastType; + priorityType: TransactionPriorityType; + maxCapType: MaxCapType; + maxCap: number; +}; + +// easy to use values for user convenience +export const enum PriotitizationFeeLevels { + LOW = 2500, + MEDIAN = 5000, + HIGH = 7500, + MAX = 10000, +} + +// extending the original interface to include the percentile and fallback options and maintain compatibility +interface GetRecentPrioritizationFeesByPercentileConfig extends GetRecentPrioritizationFeesConfig { + percentile?: PriotitizationFeeLevels | number; + fallback?: boolean; +} + +interface RpcResponse { + jsonrpc: String; + id?: String; + result?: []; + error?: any; +} + +export const getCalculatedPrioritizationFeeByPercentile = async ( + connection: Connection, + config: GetRecentPrioritizationFeesByPercentileConfig, + slotsToReturn?: number +) => { + const fees = await getRecentPrioritizationFeesByPercentile(connection, config, slotsToReturn); + + // Calculate min, max, mean + const { min, max, sum } = fees.reduce( + (acc, fee) => ({ + min: fee.prioritizationFee < acc.min.prioritizationFee ? fee : acc.min, + max: fee.prioritizationFee > acc.max.prioritizationFee ? fee : acc.max, + sum: acc.sum + fee.prioritizationFee, + }), + { min: fees[0], max: fees[0], sum: 0 } + ); + + const mean = Math.ceil(sum / fees.length); + + // Calculate median + const sortedFees = [...fees].sort((a, b) => a.prioritizationFee - b.prioritizationFee); + const midIndex = Math.floor(fees.length / 2); + + const median = + fees.length % 2 === 0 + ? Math.ceil((sortedFees[midIndex - 1].prioritizationFee + sortedFees[midIndex].prioritizationFee) / 2) + : sortedFees[midIndex].prioritizationFee; + + return { + min, + max, + mean, + median, + }; +}; + +export const getMinPrioritizationFeeByPercentile = async ( + connection: Connection, + config: GetRecentPrioritizationFeesByPercentileConfig, + slotsToReturn?: number +): Promise => { + const recentPrioritizationFees = await getRecentPrioritizationFeesByPercentile(connection, config, slotsToReturn); + + const minPriorityFee = recentPrioritizationFees.reduce((min, current) => { + return current.prioritizationFee < min.prioritizationFee ? current : min; + }); + + return minPriorityFee.prioritizationFee; +}; + +export const getMaxPrioritizationFeeByPercentile = async ( + connection: Connection, + config: GetRecentPrioritizationFeesByPercentileConfig, + slotsToReturn?: number +): Promise => { + const recentPrioritizationFees = await getRecentPrioritizationFeesByPercentile(connection, config, slotsToReturn); + + const maxPriorityFee = recentPrioritizationFees.reduce((max, current) => { + return current.prioritizationFee > max.prioritizationFee ? current : max; + }); + + return maxPriorityFee.prioritizationFee; +}; + +export const getMeanPrioritizationFeeByPercentile = async ( + connection: Connection, + config: GetRecentPrioritizationFeesByPercentileConfig, + slotsToReturn?: number +): Promise => { + const recentPrioritizationFees = await getRecentPrioritizationFeesByPercentile(connection, config, slotsToReturn); + + const mean = Math.ceil( + recentPrioritizationFees.reduce((acc, fee) => acc + fee.prioritizationFee, 0) / recentPrioritizationFees.length + ); + + return mean; +}; + +export const getMedianPrioritizationFeeByPercentile = async ( + connection: Connection, + config: GetRecentPrioritizationFeesByPercentileConfig, + slotsToReturn?: number +): Promise => { + const recentPrioritizationFees = await getRecentPrioritizationFeesByPercentile(connection, config, slotsToReturn); + + recentPrioritizationFees.sort((a, b) => a.prioritizationFee - b.prioritizationFee); + + const half = Math.floor(recentPrioritizationFees.length / 2); + + if (recentPrioritizationFees.length % 2) { + return recentPrioritizationFees[half].prioritizationFee; + } + + return Math.ceil( + (recentPrioritizationFees[half - 1].prioritizationFee + recentPrioritizationFees[half].prioritizationFee) / 2 + ); +}; + +// this function gets the recent prioritization fees from the RPC. The `rpcRequest` comes from webjs.Connection +const getRecentPrioritizationFeesFromRpc = async (config: any, rpcRequest: any) => { + const accounts = config?.lockedWritableAccounts?.map((key: { toBase58: () => any }) => key.toBase58()); + const args = accounts?.length ? [accounts] : [[]]; + config.percentile && args.push({ percentile: config.percentile }); + + const response = await rpcRequest("getRecentPrioritizationFees", args); + + return response; +}; + +export const getRecentPrioritizationFeesByPercentile = async ( + connection: Connection, + config: GetRecentPrioritizationFeesByPercentileConfig, + slotsToReturn?: number +): Promise => { + const { fallback = true, lockedWritableAccounts = [] } = config || {}; + slotsToReturn = slotsToReturn && Number.isInteger(slotsToReturn) ? slotsToReturn : -1; + + const promises = []; + + let tritonRpcResponse: RpcResponse | undefined = undefined; + let fallbackRpcResponse: RpcResponse | undefined = undefined; + + // @solana/web3.js uses the private method `_rpcRequest` internally to make RPC requests which is not exposed by TypeScript + // it is available in JavaScript, however, TypeScript enforces it as unavailable and complains, the following line is a workaround + + /* @ts-ignore */ + const rpcRequest = connection._rpcRequest; + + // to save fallback roundtrips if your RPC is not Triton, both RPCs are called in parallel to minimize latency + promises.push( + getRecentPrioritizationFeesFromRpc(config, rpcRequest).then((result) => { + tritonRpcResponse = result; + }) + ); + + if (fallback) { + promises.push( + getRecentPrioritizationFeesFromRpc({ lockedWritableAccounts }, rpcRequest).then((result) => { + fallbackRpcResponse = result; + }) + ); + } + + await Promise.all(promises); + + // satisfying typescript by casting the response to RpcResponse + const tritonGRPFResponse = tritonRpcResponse as unknown as RpcResponse; + const fallbackGRPFResponse = fallbackRpcResponse as unknown as RpcResponse; + + let recentPrioritizationFees: RecentPrioritizationFees[] = []; + + if (tritonGRPFResponse?.result) { + recentPrioritizationFees = tritonGRPFResponse.result!; + } + + if (fallbackGRPFResponse?.result && !tritonGRPFResponse?.result) { + recentPrioritizationFees = fallbackGRPFResponse.result!; + } + + if (fallback && fallbackGRPFResponse.error) { + return fallbackGRPFResponse.error; + } + + if (tritonGRPFResponse?.error) { + return tritonGRPFResponse.error; + } + + // sort the prioritization fees by slot + recentPrioritizationFees.sort((a, b) => a.slot - b.slot); + + // return the first n prioritization fees + if (slotsToReturn > 0) return recentPrioritizationFees.slice(0, slotsToReturn); + + return recentPrioritizationFees; +}; diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/lend-box/hooks/use-lend-simulation.hooks.ts b/packages/mrgn-ui/src/components/action-box-v2/actions/lend-box/hooks/use-lend-simulation.hooks.ts index be3c5ebe61..b97ad53980 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/lend-box/hooks/use-lend-simulation.hooks.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/lend-box/hooks/use-lend-simulation.hooks.ts @@ -5,8 +5,8 @@ import { Transaction, VersionedTransaction } from "@solana/web3.js"; import { AccountSummary, ActionType, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { MarginfiAccountWrapper, SimulationResult } from "@mrgnlabs/marginfi-client-v2"; import { ActionMethod, STATIC_SIMULATION_ERRORS, usePrevious } from "@mrgnlabs/mrgn-utils"; +import { TransactionBroadcastType } from "@mrgnlabs/mrgn-common"; -import { useActionBoxStore } from "../../../store"; import { calculateLendingTransaction, calculateSummary, getSimulationResult } from "../utils"; /* @@ -27,6 +27,8 @@ type LendSimulationProps = { additionalTxns: (VersionedTransaction | Transaction)[]; }; simulationResult: SimulationResult | null; + priorityFee: number; + broadcastType: TransactionBroadcastType; setSimulationResult: (result: SimulationResult | null) => void; setActionTxns: (actionTxns: { actionTxn: VersionedTransaction | Transaction | null; @@ -44,6 +46,8 @@ export function useLendSimulation({ lendMode, actionTxns, simulationResult, + priorityFee, + broadcastType, setSimulationResult, setActionTxns, setErrorMessage, @@ -51,8 +55,6 @@ export function useLendSimulation({ }: LendSimulationProps) { const prevDebouncedAmount = usePrevious(debouncedAmount); - const [priorityFee] = useActionBoxStore((state) => [state.priorityFee]); - const handleSimulation = React.useCallback( async (txns: (VersionedTransaction | Transaction)[]) => { try { @@ -119,7 +121,8 @@ export function useLendSimulation({ selectedBank, lendMode, amount, - 0 //priorityFee + priorityFee, + broadcastType ); if (lendingObject && "actionTxn" in lendingObject) { @@ -129,6 +132,7 @@ export function useLendSimulation({ setErrorMessage(errorMessage); } } catch (error) { + console.log(error); setErrorMessage(STATIC_SIMULATION_ERRORS.BUILDING_LENDING_TX); setIsLoading(false); } @@ -138,7 +142,7 @@ export function useLendSimulation({ // setIsLoading(false); // } }, - [selectedAccount, selectedBank, lendMode, setActionTxns, setErrorMessage, setIsLoading] + [selectedAccount, selectedBank, setIsLoading, setActionTxns, lendMode, priorityFee, broadcastType, setErrorMessage] ); React.useEffect(() => { diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/lend-box/lend-box.tsx b/packages/mrgn-ui/src/components/action-box-v2/actions/lend-box/lend-box.tsx index e9f2192086..7c645b241c 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/lend-box/lend-box.tsx +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/lend-box/lend-box.tsx @@ -11,7 +11,7 @@ import { computeAccountSummary, DEFAULT_ACCOUNT_SUMMARY, } from "@mrgnlabs/marginfi-v2-ui-state"; -import { ActionMethod, MarginfiActionParams, PreviousTxn } from "@mrgnlabs/mrgn-utils"; +import { ActionMethod, MarginfiActionParams, PreviousTxn, useConnection, usePriorityFee } from "@mrgnlabs/mrgn-utils"; import { MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; import { ActionButton, ActionMessage, ActionSettingsButton } from "~/components/action-box-v2/components"; @@ -26,6 +26,7 @@ import { Collateral, ActionInput, Preview } from "./components"; import { useLendSimulation } from "./hooks"; import { useActionBoxStore } from "../../store"; import { HidePoolStats } from "../../contexts/actionbox/actionbox.context"; +import { useActionContext } from "../../contexts"; // error handling export type LendBoxProps = { @@ -109,15 +110,23 @@ export const LendBox = ({ state.setErrorMessage, ]); + const { priorityType, broadcastType, maxCap, maxCapType } = useActionContext(); + + const priorityFee = usePriorityFee( + priorityType, + broadcastType, + maxCapType, + maxCap, + marginfiClient?.provider.connection + ); + const accountSummary = React.useMemo(() => { return ( accountSummaryArg ?? (selectedAccount ? computeAccountSummary(selectedAccount, banks) : DEFAULT_ACCOUNT_SUMMARY) ); }, [accountSummaryArg, selectedAccount, banks]); - const [setIsSettingsDialogOpen, priorityFee, setPreviousTxn, setIsActionComplete] = useActionBoxStore((state) => [ - state.setIsSettingsDialogOpen, - state.priorityFee, + const [setPreviousTxn, setIsActionComplete] = useActionBoxStore((state) => [ state.setPreviousTxn, state.setIsActionComplete, ]); @@ -136,6 +145,8 @@ export const LendBox = ({ lendMode, actionTxns, simulationResult, + priorityFee, + broadcastType, setSimulationResult, setActionTxns, setErrorMessage, @@ -200,7 +211,8 @@ export const LendBox = ({ params: { bank: selectedBank, marginfiAccount: selectedAccount, - priorityFee: 0, + priorityFee, + broadcastType, }, captureEvent: (event, properties) => { captureEvent && captureEvent(event, properties); @@ -236,6 +248,8 @@ export const LendBox = ({ }, [ selectedBank, selectedAccount, + priorityFee, + broadcastType, setAmountRaw, captureEvent, setIsActionComplete, @@ -249,6 +263,9 @@ export const LendBox = ({ return; } + console.log("priorityFee", priorityFee); + console.log("broadcastType", broadcastType); + const action = async () => { const params = { marginfiClient, @@ -259,7 +276,8 @@ export const LendBox = ({ marginfiAccount: selectedAccount, walletContextState, actionTxns, - priorityFee: 0, + priorityFee, + broadcastType, } as MarginfiActionParams; await handleExecuteLendingAction({ @@ -314,6 +332,8 @@ export const LendBox = ({ selectedAccount, walletContextState, actionTxns, + priorityFee, + broadcastType, captureEvent, setIsActionComplete, setPreviousTxn, @@ -369,8 +389,6 @@ export const LendBox = ({ />
- {/* */} - { - const { actionType, bank, amount, priorityFee } = params; + const { actionType, bank, amount, priorityFee, broadcastType } = params; setIsLoading(true); const attemptUuid = uuidv4(); @@ -34,6 +35,7 @@ export const handleExecuteLendingAction = async ({ tokenName: bank.meta.tokenName, amount, priorityFee, + broadcastType, }); const txnSig = await executeLendingAction(params); @@ -60,6 +62,7 @@ interface HandleCloseBalanceProps extends ExecuteActionsCallbackProps { bank: ExtendedBankInfo; marginfiAccount: MarginfiAccountWrapper | null; priorityFee?: number; + broadcastType: TransactionBroadcastType; }; } @@ -70,7 +73,7 @@ export const handleExecuteCloseBalance = async ({ setIsComplete, setIsError, }: HandleCloseBalanceProps) => { - const { bank, marginfiAccount, priorityFee } = params; + const { bank, marginfiAccount, priorityFee, broadcastType } = params; setIsLoading(true); const attemptUuid = uuidv4(); @@ -83,7 +86,7 @@ export const handleExecuteCloseBalance = async ({ }); // const { txnSig, error } = await closeBalance({ marginfiAccount: marginfiAccount, bank: bank, priorityFee }); - const txnSig = await closeBalance({ marginfiAccount: marginfiAccount, bank: bank, priorityFee }); + const txnSig = await closeBalance({ marginfiAccount: marginfiAccount, bank: bank, priorityFee, broadcastType }); setIsLoading(false); // if (error) { @@ -110,7 +113,8 @@ export async function calculateLendingTransaction( bank: ExtendedBankInfo, actionMode: ActionType, amount: number, - priorityFee: number + priorityFee: number, + broadcastType: TransactionBroadcastType ): Promise< | { actionTxn: VersionedTransaction | Transaction; @@ -120,17 +124,27 @@ export async function calculateLendingTransaction( > { switch (actionMode) { case ActionType.Deposit: - const depositTx = await marginfiAccount.makeDepositTx(amount, bank.address, { priorityFeeUi: priorityFee }); + const depositTx = await marginfiAccount.makeDepositTx( + amount, + bank.address, + { priorityFeeUi: priorityFee }, + broadcastType + ); return { actionTxn: depositTx, additionalTxns: [], // bundle tip ix is in depositTx }; case ActionType.Borrow: - const borrowTxObject = await marginfiAccount.makeBorrowTx(amount, bank.address, { - createAtas: true, - wrapAndUnwrapSol: false, - priorityFeeUi: priorityFee, - }); + const borrowTxObject = await marginfiAccount.makeBorrowTx( + amount, + bank.address, + { + createAtas: true, + wrapAndUnwrapSol: false, + priorityFeeUi: priorityFee, + }, + broadcastType + ); return { actionTxn: borrowTxObject.borrowTx, additionalTxns: borrowTxObject.feedCrankTxs, @@ -139,7 +153,9 @@ export async function calculateLendingTransaction( const withdrawTxObject = await marginfiAccount.makeWithdrawTx( amount, bank.address, - bank.isActive && isWholePosition(bank, amount) + bank.isActive && isWholePosition(bank, amount), + { priorityFeeUi: priorityFee }, + broadcastType ); return { actionTxn: withdrawTxObject.withdrawTx, @@ -150,7 +166,8 @@ export async function calculateLendingTransaction( amount, bank.address, bank.isActive && isWholePosition(bank, amount), - { priorityFeeUi: priorityFee } + { priorityFeeUi: priorityFee }, + broadcastType ); return { actionTxn: repayTx, diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/hooks/use-loop-simulation.hooks.ts b/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/hooks/use-loop-simulation.hooks.ts index 7fb711fb7f..8bef2266f8 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/hooks/use-loop-simulation.hooks.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/hooks/use-loop-simulation.hooks.ts @@ -15,6 +15,7 @@ import { STATIC_SIMULATION_ERRORS, usePrevious, } from "@mrgnlabs/mrgn-utils"; +import { TransactionBroadcastType } from "@mrgnlabs/mrgn-common"; import { AccountSummary, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { useActionBoxStore } from "../../../store"; @@ -31,6 +32,8 @@ type LoopSimulationProps = { actionTxns: LoopActionTxns; simulationResult: SimulationResult | null; isRefreshTxn: boolean; + priorityFee: number; + broadcastType: TransactionBroadcastType; setSimulationResult: (simulationResult: SimulationResult | null) => void; setActionTxns: (actionTxns: LoopActionTxns) => void; @@ -50,6 +53,8 @@ export function useLoopSimulation({ actionTxns, simulationResult, isRefreshTxn, + priorityFee, + broadcastType, setSimulationResult, setActionTxns, @@ -57,11 +62,7 @@ export function useLoopSimulation({ setIsLoading, setMaxLeverage, }: LoopSimulationProps) { - const [slippageBps, priorityFee, platformFeeBps] = useActionBoxStore((state) => [ - state.slippageBps, - state.priorityFee, - state.platformFeeBps, - ]); + const [slippageBps, platformFeeBps] = useActionBoxStore((state) => [state.slippageBps, state.platformFeeBps]); const prevDebouncedAmount = usePrevious(debouncedAmount); const prevDebouncedLeverage = usePrevious(debouncedLeverage); @@ -146,8 +147,9 @@ export function useLoopSimulation({ amount, slippageBps, marginfiClient.provider.connection, - 0, - platformFeeBps + priorityFee, + platformFeeBps, + broadcastType ); if (loopingObject && "loopingTxn" in loopingObject) { @@ -184,6 +186,8 @@ export function useLoopSimulation({ setSimulationResult, slippageBps, priorityFee, + platformFeeBps, + broadcastType, setErrorMessage, ] ); diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/loop-box.tsx b/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/loop-box.tsx index c41338f265..c71a7a00ff 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/loop-box.tsx +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/loop-box.tsx @@ -9,7 +9,14 @@ import { ActiveBankInfo, } from "@mrgnlabs/marginfi-v2-ui-state"; import { WalletContextState } from "@solana/wallet-adapter-react"; -import { ActionMethod, MarginfiActionParams, PreviousTxn, showErrorToast } from "@mrgnlabs/mrgn-utils"; +import { + ActionMethod, + MarginfiActionParams, + PreviousTxn, + showErrorToast, + useConnection, + usePriorityFee, +} from "@mrgnlabs/mrgn-utils"; import { MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; import { useAmountDebounce } from "~/hooks/useAmountDebounce"; @@ -27,6 +34,7 @@ import { useLoopBoxStore } from "./store"; import { useLoopSimulation } from "./hooks"; import { LeverageSlider } from "./components/leverage-slider"; import { ApyStat } from "./components/apy-stat"; +import { useActionContext } from "../../contexts"; // error handling export type LoopBoxProps = { @@ -60,8 +68,6 @@ export const LoopBox = ({ onComplete, captureEvent, }: LoopBoxProps) => { - const priorityFee = 0; - const [ leverage, maxLeverage, @@ -110,6 +116,16 @@ export const LoopBox = ({ state.setIsLoading, ]); + const { priorityType, broadcastType, maxCap, maxCapType } = useActionContext(); + + const priorityFee = usePriorityFee( + priorityType, + broadcastType, + maxCapType, + maxCap, + marginfiClient?.provider.connection + ); + const [slippage, setIsSettingsDialogOpen, setPreviousTxn, setIsActionComplete] = useActionBoxStore((state) => [ state.slippageBps, state.setIsSettingsDialogOpen, @@ -154,6 +170,8 @@ export const LoopBox = ({ actionTxns, simulationResult, isRefreshTxn, + priorityFee, + broadcastType, setMaxLeverage, setSimulationResult, setActionTxns, @@ -217,7 +235,8 @@ export const LoopBox = ({ loopingBank: selectedSecondaryBank, connection: marginfiClient?.provider.connection!, }, - priorityFee: 0, + priorityFee, + broadcastType, slippage: slippage, } as MarginfiActionParams; @@ -261,11 +280,13 @@ export const LoopBox = ({ }, [ actionTxns, amount, + broadcastType, captureEvent, leverage, marginfiClient, nativeSolBalance, onComplete, + priorityFee, selectedAccount, selectedBank, selectedSecondaryBank, diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/utils/loop-action.utils.ts b/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/utils/loop-action.utils.ts index c2c0f345b4..c6e7007785 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/utils/loop-action.utils.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/loop-box/utils/loop-action.utils.ts @@ -4,6 +4,7 @@ import { Connection, VersionedTransaction } from "@solana/web3.js"; import { MarginfiAccountWrapper } from "@mrgnlabs/marginfi-client-v2"; import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; +import { TransactionBroadcastType } from "@mrgnlabs/mrgn-common"; import { ActionMethod, calculateLoopingParams, @@ -65,7 +66,8 @@ export async function calculateLooping( slippageBps: number, connection: Connection, priorityFee: number, - platformFeeBps: number + platformFeeBps: number, + broadcastType: TransactionBroadcastType ): Promise { // TODO setup logging again // capture("looper", { @@ -86,6 +88,7 @@ export async function calculateLooping( connection, priorityFee, platformFeeBps, + broadcastType, }); return result; diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/hooks/use-repay-simulation.hooks.ts b/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/hooks/use-repay-simulation.hooks.ts index 1b8a97d67b..a750c0a9ee 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/hooks/use-repay-simulation.hooks.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/hooks/use-repay-simulation.hooks.ts @@ -10,6 +10,7 @@ import { STATIC_SIMULATION_ERRORS, usePrevious, } from "@mrgnlabs/mrgn-utils"; +import { TransactionBroadcastType } from "@mrgnlabs/mrgn-common"; import { AccountSummary, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { useActionBoxStore } from "../../../store"; @@ -25,6 +26,8 @@ type RepayCollatSimulationProps = { actionTxns: RepayCollatActionTxns; simulationResult: SimulationResult | null; isRefreshTxn: boolean; + priorityFee: number; + broadcastType: TransactionBroadcastType; setSimulationResult: (simulationResult: SimulationResult | null) => void; setActionTxns: (actionTxns: RepayCollatActionTxns) => void; @@ -44,6 +47,8 @@ export function useRepayCollatSimulation({ actionTxns, simulationResult, isRefreshTxn, + priorityFee, + broadcastType, setSimulationResult, setActionTxns, @@ -52,11 +57,7 @@ export function useRepayCollatSimulation({ setIsLoading, setMaxAmountCollateral, }: RepayCollatSimulationProps) { - const [slippageBps, priorityFee, platformFeeBps] = useActionBoxStore((state) => [ - state.slippageBps, - state.priorityFee, - state.platformFeeBps, - ]); + const [slippageBps, platformFeeBps] = useActionBoxStore((state) => [state.slippageBps, state.platformFeeBps]); const prevDebouncedAmount = usePrevious(debouncedAmount); const prevSelectedSecondaryBank = usePrevious(selectedSecondaryBank); @@ -123,8 +124,9 @@ export function useRepayCollatSimulation({ amount, slippageBps, marginfiClient.provider.connection, - 0, //priorityFee, - platformFeeBps + priorityFee, + platformFeeBps, + broadcastType ); if (repayObject && "repayTxn" in repayObject) { @@ -158,7 +160,9 @@ export function useRepayCollatSimulation({ setActionTxns, setSimulationResult, slippageBps, + priorityFee, platformFeeBps, + broadcastType, setRepayAmount, setErrorMessage, ] diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/repay-collat-box.tsx b/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/repay-collat-box.tsx index 23057fd7d5..b41dbcf338 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/repay-collat-box.tsx +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/repay-collat-box.tsx @@ -8,7 +8,14 @@ import { DEFAULT_ACCOUNT_SUMMARY, ActiveBankInfo, } from "@mrgnlabs/marginfi-v2-ui-state"; -import { ActionMethod, MarginfiActionParams, PreviousTxn, showErrorToast } from "@mrgnlabs/mrgn-utils"; +import { + ActionMethod, + MarginfiActionParams, + PreviousTxn, + showErrorToast, + useConnection, + usePriorityFee, +} from "@mrgnlabs/mrgn-utils"; import { MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; import { CircularProgress } from "~/components/ui/circular-progress"; @@ -22,6 +29,7 @@ import { useRepayCollatBoxStore } from "./store"; import { useRepayCollatSimulation } from "./hooks"; import { useActionBoxStore } from "../../store"; +import { useActionContext } from "../../contexts"; // error handling export type RepayCollatBoxProps = { @@ -55,8 +63,6 @@ export const RepayCollatBox = ({ onComplete, captureEvent, }: RepayCollatBoxProps) => { - const priorityFee = 0; - const [ maxAmountCollateral, repayAmount, @@ -101,6 +107,15 @@ export const RepayCollatBox = ({ state.setIsLoading, ]); + const { priorityType, broadcastType, maxCap, maxCapType } = useActionContext(); + const priorityFee = usePriorityFee( + priorityType, + broadcastType, + maxCapType, + maxCap, + marginfiClient?.provider.connection + ); + const { isRefreshTxn, blockProgress } = usePollBlockHeight( marginfiClient?.provider.connection, actionTxns.lastValidBlockHeight @@ -142,6 +157,8 @@ export const RepayCollatBox = ({ actionTxns, simulationResult, isRefreshTxn, + priorityFee, + broadcastType, setSimulationResult, setActionTxns, setErrorMessage, @@ -196,7 +213,8 @@ export const RepayCollatBox = ({ nativeSolBalance, marginfiAccount: selectedAccount, actionTxns, - priorityFee: 0, + priorityFee, + broadcastType, } as MarginfiActionParams; await handleExecuteRepayCollatAction({ @@ -237,10 +255,12 @@ export const RepayCollatBox = ({ }, [ actionTxns, amount, + broadcastType, captureEvent, marginfiClient, nativeSolBalance, onComplete, + priorityFee, selectedAccount, selectedBank, setAmountRaw, diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/utils/repay-action.utils.ts b/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/utils/repay-action.utils.ts index 9f41ff1cfd..a16a32a669 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/utils/repay-action.utils.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/repay-collat-box/utils/repay-action.utils.ts @@ -4,6 +4,7 @@ import { Connection, VersionedTransaction } from "@solana/web3.js"; import { MarginfiAccountWrapper } from "@mrgnlabs/marginfi-client-v2"; import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; +import { TransactionBroadcastType } from "@mrgnlabs/mrgn-common"; import { ActionMethod, calculateRepayCollateralParams, @@ -63,7 +64,8 @@ export async function calculateRepayCollateral( slippageBps: number, connection: Connection, priorityFee: number, - platformFeeBps: number + platformFeeBps: number, + broadcastType: TransactionBroadcastType ): Promise< | { repayTxn: VersionedTransaction; @@ -91,7 +93,8 @@ export async function calculateRepayCollateral( slippageBps, connection, priorityFee, - platformFeeBps + platformFeeBps, + broadcastType ); return result; diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/hooks/use-stake-simulation.hooks.tsx b/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/hooks/use-stake-simulation.hooks.tsx index 4cc796b32c..5ac5928624 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/hooks/use-stake-simulation.hooks.tsx +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/hooks/use-stake-simulation.hooks.tsx @@ -15,7 +15,13 @@ import { STATIC_SIMULATION_ERRORS, usePrevious, } from "@mrgnlabs/mrgn-utils"; -import { LST_MINT, LUT_PROGRAM_AUTHORITY_INDEX, NATIVE_MINT as SOL_MINT, uiToNative } from "@mrgnlabs/mrgn-common"; +import { + LST_MINT, + LUT_PROGRAM_AUTHORITY_INDEX, + NATIVE_MINT as SOL_MINT, + TransactionBroadcastType, + uiToNative, +} from "@mrgnlabs/mrgn-common"; import { getAdressLookupTableAccounts, getSimulationResult, handleStakeTx } from "../utils"; import { useActionBoxStore } from "../../../store"; @@ -29,6 +35,8 @@ type StakeSimulationProps = { actionTxns: StakeActionTxns; simulationResult: any | null; marginfiClient: MarginfiClient | null; + priorityFee: number; + broadcastType: TransactionBroadcastType; setSimulationResult: (result: any | null) => void; setActionTxns: (actionTxns: StakeActionTxns) => void; setErrorMessage: (error: ActionMethod | null) => void; @@ -43,6 +51,8 @@ export function useStakeSimulation({ actionTxns, simulationResult, marginfiClient, + priorityFee, + broadcastType, setSimulationResult, setActionTxns, @@ -166,7 +176,9 @@ export function useStakeSimulation({ selectedBank, marginfiClient, connection, - lstData + lstData, + priorityFee, + broadcastType ); setActionTxns(_actionTxns); } else { diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/stake-box.tsx b/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/stake-box.tsx index a387f11a43..339e126642 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/stake-box.tsx +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/stake-box.tsx @@ -4,7 +4,14 @@ import { WalletContextState } from "@solana/wallet-adapter-react"; import { getPriceWithConfidence, MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; import { AccountSummary, ActionType, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; -import { ActionMethod, LstData, PreviousTxn, showErrorToast, STATIC_SIMULATION_ERRORS } from "@mrgnlabs/mrgn-utils"; +import { + ActionMethod, + LstData, + PreviousTxn, + showErrorToast, + STATIC_SIMULATION_ERRORS, + usePriorityFee, +} from "@mrgnlabs/mrgn-utils"; import { nativeToUi, NATIVE_MINT as SOL_MINT, uiToNative } from "@mrgnlabs/mrgn-common"; import { useActionAmounts, usePollBlockHeight } from "~/components/action-box-v2/hooks"; @@ -19,7 +26,7 @@ import { useActionBoxStore } from "../../store"; import { handleExecuteLstAction } from "./utils/stake-action.utils"; import { ActionInput } from "./components/action-input"; import { checkActionAvailable } from "./utils"; -import { useStakeBoxContext } from "../../contexts"; +import { useActionContext, useStakeBoxContext } from "../../contexts"; export type StakeBoxProps = { nativeSolBalance: number; @@ -97,6 +104,16 @@ export const StakeBox = ({ actionMode, }); + const { priorityType, broadcastType, maxCap, maxCapType } = useActionContext(); + + const priorityFee = usePriorityFee( + priorityType, + broadcastType, + maxCapType, + maxCap, + marginfiClient?.provider.connection + ); + const [setIsSettingsDialogOpen, setPreviousTxn, setIsActionComplete] = useActionBoxStore((state) => [ state.setIsSettingsDialogOpen, state.setPreviousTxn, @@ -146,6 +163,8 @@ export const StakeBox = ({ setIsLoading, marginfiClient, lstData, + priorityFee, + broadcastType, }); const actionSummary = React.useMemo(() => { @@ -167,6 +186,13 @@ export const StakeBox = ({ const params = { actionTxns, marginfiClient, + actionType: requestedActionType, + nativeSolBalance, + broadcastType, + originDetails: { + amount, + tokenSymbol: selectedBank.meta.tokenSymbol, + }, }; await handleExecuteLstAction({ @@ -205,12 +231,6 @@ export const StakeBox = ({ }, setIsError: () => {}, setIsLoading: (isLoading) => setIsLoading({ type: "TRANSACTION", state: isLoading }), - actionType: requestedActionType, - nativeSolBalance, - originDetails: { - amount, - tokenSymbol: selectedBank.meta.tokenSymbol, - }, }); }; @@ -223,15 +243,16 @@ export const StakeBox = ({ amount, marginfiClient, setAmountRaw, + setIsLoading, actionTxns, requestedActionType, - receiveAmount, + nativeSolBalance, + broadcastType, captureEvent, setIsActionComplete, setPreviousTxn, + receiveAmount, onComplete, - setIsLoading, - nativeSolBalance, ]); const actionMethods = React.useMemo(() => { diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/utils/stake-action.utils.ts b/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/utils/stake-action.utils.ts index ea15b14c78..b27bdd45d1 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/utils/stake-action.utils.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/utils/stake-action.utils.ts @@ -6,19 +6,20 @@ import { MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-clien import { extractErrorString, MultiStepToastHandle, showErrorToast, StakeActionTxns } from "@mrgnlabs/mrgn-utils"; import { ExecuteActionsCallbackProps } from "~/components/action-box-v2/types"; -import { numeralFormatter, nativeToUi } from "@mrgnlabs/mrgn-common"; +import { numeralFormatter, nativeToUi, TransactionBroadcastType } from "@mrgnlabs/mrgn-common"; interface ExecuteStakeActionProps extends ExecuteActionsCallbackProps { params: { actionTxns: StakeActionTxns; marginfiClient: MarginfiClient; - }; - actionType: ActionType; - nativeSolBalance: number; - selectedAccount?: MarginfiAccountWrapper; - originDetails: { - amount: number; - tokenSymbol: string; + actionType: ActionType; + nativeSolBalance: number; + selectedAccount?: MarginfiAccountWrapper; + originDetails: { + amount: number; + tokenSymbol: string; + }; + broadcastType: TransactionBroadcastType; }; } @@ -28,11 +29,8 @@ export const handleExecuteLstAction = async ({ setIsLoading, setIsComplete, setIsError, - actionType, - nativeSolBalance, - originDetails, }: ExecuteStakeActionProps) => { - const { actionTxns, marginfiClient } = params; + const { actionTxns, marginfiClient, actionType, nativeSolBalance, originDetails, broadcastType } = params; setIsLoading(true); const attemptUuid = uuidv4(); @@ -48,6 +46,7 @@ export const handleExecuteLstAction = async ({ actionType, nativeSolBalance, originDetails, + broadcastType, }); setIsLoading(false); @@ -79,6 +78,7 @@ const executeLstAction = async ({ actionType, nativeSolBalance, originDetails, + broadcastType, }: { marginfiClient: MarginfiClient; actionTxns: StakeActionTxns; @@ -88,6 +88,7 @@ const executeLstAction = async ({ amount: number; tokenSymbol: string; }; + broadcastType: TransactionBroadcastType; }) => { if (!actionTxns.actionTxn) return; if (nativeSolBalance < FEE_MARGIN) { @@ -113,18 +114,20 @@ const executeLstAction = async ({ Number(originDetails.amount) < 0.01 ? "<0.01" : numeralFormatter(Number(originDetails.amount)) } LST`; - const multiStepToast = new MultiStepToastHandle( - actionType === ActionType.MintLST ? `Minting LST` : `Unstaking LST`, - [ - { - label: toastLabels, - }, - ] - ); + const multiStepToast = new MultiStepToastHandle(actionType === ActionType.MintLST ? `Minting LST` : `Unstaking LST`, [ + { + label: toastLabels, + }, + ]); multiStepToast.start(); try { - const txnSig = await marginfiClient.processTransactions([actionTxns.actionTxn, ...actionTxns.additionalTxns]); + const txnSig = await marginfiClient.processTransactions( + [actionTxns.actionTxn, ...actionTxns.additionalTxns], + undefined, + undefined, + broadcastType + ); multiStepToast.setSuccessAndNext(); return txnSig; diff --git a/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/utils/stake-simulation.utils.ts b/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/utils/stake-simulation.utils.ts index 178ed54507..0715e60ad3 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/utils/stake-simulation.utils.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/actions/stake-box/utils/stake-simulation.utils.ts @@ -9,6 +9,7 @@ import { import { ExtendedBankInfo, AccountSummary } from "@mrgnlabs/marginfi-v2-ui-state"; import { LST_MINT, + TransactionBroadcastType, createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, uiToNative, @@ -135,7 +136,9 @@ export async function handleStakeTx( selectedBank: ExtendedBankInfo, marginfiClient: MarginfiClient, connection: Connection, - lstData: LstData + lstData: LstData, + priorityFee: number, + broadcastType: TransactionBroadcastType ) { const stakeAmount = swapQuote ? Number(swapQuote.outAmount) @@ -190,7 +193,7 @@ export async function handleStakeTx( }) ); - const bundleTipIx = makeBundleTipIx(marginfiClient.wallet.publicKey); + const bundleTipIx = makeBundleTipIx(marginfiClient.wallet.publicKey, priorityFee); const stakeMessage = new TransactionMessage({ payerKey: marginfiClient.wallet.publicKey, diff --git a/packages/mrgn-ui/src/components/action-box-v2/components/action-settings/action-settings.tsx b/packages/mrgn-ui/src/components/action-box-v2/components/action-settings/action-settings.tsx index 679bae11d1..1eb7cab3d9 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/components/action-settings/action-settings.tsx +++ b/packages/mrgn-ui/src/components/action-box-v2/components/action-settings/action-settings.tsx @@ -1,43 +1,30 @@ import React from "react"; import { IconArrowLeft } from "@tabler/icons-react"; -import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group"; - -import { Slippage, PriorityFees } from "./components"; +import { Slippage } from "./components"; interface ActionSettingsProps { - priorityFee?: number; slippage?: number; - changePriorityFee: (value: number) => void; changeSlippage: (value: number) => void; - toggleSettings: (value: boolean) => void; returnLabel?: string; } -enum SettingsState { - Slippage = "slippage", - PriorityFee = "priority-fee", -} - export const ActionSettings = ({ - priorityFee, slippage, - changePriorityFee, changeSlippage, toggleSettings, returnLabel = "Back", }: ActionSettingsProps) => { - const [settingsMode, setSettingsMode] = React.useState(SettingsState.PriorityFee); - return (
+ {/* Navigator logic if we have more settings */} {/* {slippage !== undefined && ( - Priority Fee + Setting 2 - )} */} + )} */}
{slippage !== undefined && ( @@ -64,9 +51,6 @@ export const ActionSettings = ({ setSlippagePct={(value) => changeSlippage(value * 100)} /> )} - {/* {priorityFee !== undefined && settingsMode === SettingsState.PriorityFee && ( - - )} */}
); diff --git a/packages/mrgn-ui/src/components/action-box-v2/components/action-wrappers/action-box-wrapper.tsx b/packages/mrgn-ui/src/components/action-box-v2/components/action-wrappers/action-box-wrapper.tsx index bbc61bb4bf..9f9ec7e090 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/components/action-wrappers/action-box-wrapper.tsx +++ b/packages/mrgn-ui/src/components/action-box-v2/components/action-wrappers/action-box-wrapper.tsx @@ -14,15 +14,12 @@ interface ActionBoxWrapperProps { } export const ActionBoxWrapper = ({ children, isDialog, actionMode, showSettings = true }: ActionBoxWrapperProps) => { - const [priorityFee, slippage, isSettingsDialogOpen, setIsSettingsDialogOpen, setPriorityFee, setSlippageBps] = - useActionBoxStore((state) => [ - state.priorityFee, - state.slippageBps, - state.isSettingsDialogOpen, - state.setIsSettingsDialogOpen, - state.setPriorityFee, - state.setSlippageBps, - ]); + const [slippage, isSettingsDialogOpen, setIsSettingsDialogOpen, setSlippageBps] = useActionBoxStore((state) => [ + state.slippageBps, + state.isSettingsDialogOpen, + state.setIsSettingsDialogOpen, + state.setSlippageBps, + ]); const isActionDisabled = React.useMemo(() => { const blockedActions = getBlockedActions(); @@ -66,9 +63,7 @@ export const ActionBoxWrapper = ({ children, isDialog, actionMode, showSettings > {isSettingsDialogOpen && showSettings ? ( setIsSettingsDialogOpen(value)} /> diff --git a/packages/mrgn-ui/src/components/action-box-v2/contexts/action/action.context.tsx b/packages/mrgn-ui/src/components/action-box-v2/contexts/action/action.context.tsx new file mode 100644 index 0000000000..56036740dc --- /dev/null +++ b/packages/mrgn-ui/src/components/action-box-v2/contexts/action/action.context.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +import { MaxCapType, TransactionBroadcastType, TransactionPriorityType } from "@mrgnlabs/mrgn-common"; +import { DEFAULT_PRIORITY_SETTINGS } from "@mrgnlabs/mrgn-utils"; + +type ActionContextType = { + priorityType: TransactionPriorityType; + broadcastType: TransactionBroadcastType; + maxCap: number; + maxCapType: MaxCapType; +}; + +const ActionContext = React.createContext({ ...DEFAULT_PRIORITY_SETTINGS }); + +export const ActionProvider: React.FC = ({ children, ...props }) => { + return {children}; +}; + +export const useActionContext = () => { + const context = React.useContext(ActionContext); + return context; +}; diff --git a/packages/mrgn-ui/src/components/action-box-v2/contexts/action/index.ts b/packages/mrgn-ui/src/components/action-box-v2/contexts/action/index.ts new file mode 100644 index 0000000000..b37eafcb27 --- /dev/null +++ b/packages/mrgn-ui/src/components/action-box-v2/contexts/action/index.ts @@ -0,0 +1 @@ +export * from "./action.context"; diff --git a/packages/mrgn-ui/src/components/action-box-v2/contexts/index.ts b/packages/mrgn-ui/src/components/action-box-v2/contexts/index.ts index 7470058662..919f73f6d1 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/contexts/index.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/contexts/index.ts @@ -1,2 +1,3 @@ export * from "./actionbox"; export * from "./stakebox"; +export * from "./action"; diff --git a/packages/mrgn-ui/src/components/action-box-v2/store/action-box-store.ts b/packages/mrgn-ui/src/components/action-box-v2/store/action-box-store.ts index 2e81225c04..d63c52d5f2 100644 --- a/packages/mrgn-ui/src/components/action-box-v2/store/action-box-store.ts +++ b/packages/mrgn-ui/src/components/action-box-v2/store/action-box-store.ts @@ -5,7 +5,6 @@ import { PreviousTxn } from "@mrgnlabs/mrgn-utils"; interface ActionBoxState { // State isSettingsDialogOpen: boolean; - priorityFee: number; slippageBps: number; platformFeeBps: number; isActionComplete: boolean; @@ -15,7 +14,6 @@ interface ActionBoxState { setIsActionComplete: (isActionSuccess: boolean) => void; setPreviousTxn: (previousTxn: PreviousTxn) => void; setIsSettingsDialogOpen: (isOpen: boolean) => void; - setPriorityFee: (priorityFee: number) => void; setSlippageBps: (slippageBps: number) => void; setPlatformFeeBps: (platformFeeBps: number) => void; } @@ -24,12 +22,7 @@ function createActionBoxStore() { return create()( persist(stateCreator, { name: "actionBoxStore", - onRehydrateStorage: () => (state) => { - // overwrite priority fee - if (process.env.NEXT_PUBLIC_INIT_PRIO_FEE && process.env.NEXT_PUBLIC_INIT_PRIO_FEE !== "0") { - state?.setPriorityFee(Number(process.env.NEXT_PUBLIC_INIT_PRIO_FEE)); - } - }, + onRehydrateStorage: () => (state) => {}, }) ); } @@ -45,7 +38,6 @@ const stateCreator: StateCreator = (set, get) => ({ // Actions setIsSettingsDialogOpen: (isOpen: boolean) => set({ isSettingsDialogOpen: isOpen }), - setPriorityFee: (priorityFee: number) => set({ priorityFee: priorityFee }), setSlippageBps: (slippageBps: number) => set({ slippageBps: slippageBps }), setIsActionComplete: (isActionSuccess: boolean) => set({ isActionComplete: isActionSuccess }), setPreviousTxn: (previousTxn: PreviousTxn) => set({ previousTxn: previousTxn }), diff --git a/packages/mrgn-ui/src/components/index.ts b/packages/mrgn-ui/src/components/index.ts index 6ebc975f20..a06371a27f 100644 --- a/packages/mrgn-ui/src/components/index.ts +++ b/packages/mrgn-ui/src/components/index.ts @@ -2,3 +2,4 @@ export * from "./action-box-v2"; export * from "./wallet-v2"; export * from "./LSTDialog"; export * from "./action-complete"; +export * from "./settings"; diff --git a/packages/mrgn-ui/src/components/settings/index.ts b/packages/mrgn-ui/src/components/settings/index.ts new file mode 100644 index 0000000000..dcf101b0c6 --- /dev/null +++ b/packages/mrgn-ui/src/components/settings/index.ts @@ -0,0 +1 @@ +export * from "./settings"; diff --git a/packages/mrgn-ui/src/components/settings/settings.tsx b/packages/mrgn-ui/src/components/settings/settings.tsx new file mode 100644 index 0000000000..67ed33392d --- /dev/null +++ b/packages/mrgn-ui/src/components/settings/settings.tsx @@ -0,0 +1,223 @@ +import React from "react"; +import { useForm } from "react-hook-form"; + +import { cn } from "@mrgnlabs/mrgn-utils"; +import { MaxCapType, TransactionBroadcastType, TransactionPriorityType } from "@mrgnlabs/mrgn-common"; + +import { Form, FormControl, FormField, FormItem } from "~/components/ui/form"; +import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group"; +import { Label } from "~/components/ui/label"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; + +type SettingsOptions = { + broadcastType: TransactionBroadcastType; + priorityType: TransactionPriorityType; + maxCapType: MaxCapType; + maxCap: number; +}; + +const broadcastTypes: { type: TransactionBroadcastType; label: string }[] = [ + { type: "BUNDLE", label: "Jito Bundles" }, + { type: "RPC", label: "RPC Priority Fees" }, +]; + +const maxCapTypes: { type: MaxCapType; label: string }[] = [ + { type: "DYNAMIC", label: "Dynamic" }, + { type: "MANUAL", label: "Manual" }, +]; + +const priorityTypes: { type: TransactionPriorityType; label: string }[] = [ + { type: "NORMAL", label: "Normal" }, + { type: "HIGH", label: "High" }, + { type: "MAMAS", label: "Mamas" }, +]; + +interface SettingsForm extends SettingsOptions {} + +interface SettingsProps extends SettingsOptions { + recommendedBroadcastType?: TransactionBroadcastType; + onChange: (options: SettingsOptions) => void; +} + +export const Settings = ({ onChange, recommendedBroadcastType = "BUNDLE", ...props }: SettingsProps) => { + const form = useForm({ + defaultValues: props, + }); + + const formValues = form.watch(); + + function onSubmit(data: SettingsForm) { + onChange(data); + form.reset(data); + } + + return ( +
+
+ + {/* Add this again if sequential transaction are more stabel */} + {/*
+

Transaction Method

+

Choose how transactions are broadcasted to the network.

+ ( + + + value === "BUNDLE" && field.onChange(value)} + defaultValue={field.value.toString()} + className="flex justify-between" + > + {broadcastTypes.map((option) => ( +
+ + + {option.type === recommendedBroadcastType && ( + + Suggested + + )} +
+ ))} +
+
+
+ )} + /> +
*/} + {/*
*/} +
+

Transaction Priority

+ ( + + + field.onChange(value)} + defaultValue={field.value.toString()} + className="flex justify-between" + > + {priorityTypes.map((option) => ( +
+ + +
+ ))} +
+
+
+ )} + /> +
+
+

Set Priority Fee Cap

+

+ Set the maximum fee you are willing to pay for a transaction. +

+ + ( + + + field.onChange(value)} + defaultValue={field.value?.toString()} + className="flex justify-between" + > + {maxCapTypes.map((option) => ( +
+ + +
+ ))} +
+
+
+ )} + /> + + ( + + +
+ field.onChange(e)} + className={cn( + "h-auto bg-mfi-action-box-background-dark py-3 px-4 border border-transparent transition-colors focus-visible:ring-0", + "focussed:border-mfi-action-box-highlight" + )} + /> + SOL +
+
+
+ )} + /> +
+
+ + {form.formState.isDirty ? ( +
You have unsaved changes.
+ ) : ( + form.formState.isSubmitted &&
Settings saved!
+ )} +
+ + +
+ ); +}; diff --git a/packages/mrgn-utils/src/actions/flashloans/builders.ts b/packages/mrgn-utils/src/actions/flashloans/builders.ts index dd9cb94a57..0df5703859 100644 --- a/packages/mrgn-utils/src/actions/flashloans/builders.ts +++ b/packages/mrgn-utils/src/actions/flashloans/builders.ts @@ -4,7 +4,7 @@ import BigNumber from "bignumber.js"; import { MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; import { ActiveBankInfo, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; -import { LUT_PROGRAM_AUTHORITY_INDEX, nativeToUi, uiToNative } from "@mrgnlabs/mrgn-common"; +import { LUT_PROGRAM_AUTHORITY_INDEX, nativeToUi, TransactionBroadcastType, uiToNative } from "@mrgnlabs/mrgn-common"; import { deserializeInstruction, getAdressLookupTableAccounts, getSwapQuoteWithRetry } from "../helpers"; import { isWholePosition } from "../../mrgnUtils"; @@ -43,7 +43,8 @@ export async function calculateRepayCollateralParams( slippageBps: number, connection: Connection, priorityFee: number, - platformFeeBps?: number + platformFeeBps: number, + broadcastType: TransactionBroadcastType ): Promise< | { repayTxn: VersionedTransaction; @@ -94,7 +95,7 @@ export async function calculateRepayCollateralParams( swapQuote, connection, priorityFee, - isTxnSplit + broadcastType ); if (txn.flashloanTx) { return { @@ -213,7 +214,7 @@ export async function calculateLoopingParams({ connection, priorityFee, platformFeeBps, - isTrading, + broadcastType, }: { marginfiAccount: MarginfiAccountWrapper | null; marginfiClient?: MarginfiClient; @@ -226,6 +227,7 @@ export async function calculateLoopingParams({ priorityFee: number; platformFeeBps?: number; isTrading?: boolean; + broadcastType: TransactionBroadcastType; }): Promise { if (!marginfiAccount && !marginfiClient) { return STATIC_SIMULATION_ERRORS.NOT_INITIALIZED; @@ -311,8 +313,8 @@ export async function calculateLoopingParams({ borrowAmount, swapQuote, connection, - // isTxnSplit, - priorityFee + priorityFee, + broadcastType ); } if (txn.flashloanTx || !marginfiAccount) { @@ -347,6 +349,7 @@ export async function calculateLoopingTransaction({ priorityFee, loopObject, isTrading, + broadcastType, }: { marginfiAccount: MarginfiAccountWrapper | null; borrowBank: ExtendedBankInfo; @@ -355,6 +358,7 @@ export async function calculateLoopingTransaction({ priorityFee: number; loopObject?: LoopingObject; isTrading?: boolean; + broadcastType: TransactionBroadcastType; }): Promise { if (loopObject && marginfiAccount) { const txn = await verifyTxSizeLooping( @@ -365,8 +369,8 @@ export async function calculateLoopingTransaction({ loopObject.borrowAmount, loopObject.quote, connection, - // false, - priorityFee + priorityFee, + broadcastType ); if (!txn) { @@ -396,14 +400,15 @@ export async function loopingBuilder({ depositAmount, options, priorityFee, + broadcastType, }: // isTxnSplit, { marginfiAccount: MarginfiAccountWrapper; bank: ExtendedBankInfo; depositAmount: number; options: LoopingOptions; - priorityFee?: number; - // isTxnSplit: boolean; + priorityFee: number; + broadcastType: TransactionBroadcastType; }): Promise<{ flashloanTx: VersionedTransaction; feedCrankTxs: VersionedTransaction[]; @@ -448,8 +453,8 @@ export async function loopingBuilder({ [swapIx], swapLUTs, priorityFee, - true - // isTxnSplit + true, // deprecated remove + broadcastType ); return { flashloanTx, feedCrankTxs, addressLookupTableAccounts }; @@ -464,14 +469,14 @@ export async function repayWithCollatBuilder({ amount, options, priorityFee, - isTxnSplit, + broadcastType, }: { marginfiAccount: MarginfiAccountWrapper; bank: ExtendedBankInfo; amount: number; options: RepayWithCollatOptions; - priorityFee?: number; - isTxnSplit: boolean; + priorityFee: number; + broadcastType: TransactionBroadcastType; }) { const jupiterQuoteApi = createJupiterApiClient(); let feeAccountInfo: AccountInfo | null = null; @@ -509,8 +514,9 @@ export async function repayWithCollatBuilder({ [swapIx], swapLUTs, priorityFee, - isTxnSplit, - blockhash + false, + blockhash, + broadcastType ); return { flashloanTx, feedCrankTxs, addressLookupTableAccounts, lastValidBlockHeight }; diff --git a/packages/mrgn-utils/src/actions/flashloans/helpers.ts b/packages/mrgn-utils/src/actions/flashloans/helpers.ts index 9b79138cc1..77d8285047 100644 --- a/packages/mrgn-utils/src/actions/flashloans/helpers.ts +++ b/packages/mrgn-utils/src/actions/flashloans/helpers.ts @@ -1,15 +1,15 @@ import { AddressLookupTableAccount, Connection, VersionedTransaction } from "@solana/web3.js"; +import BigNumber from "bignumber.js"; +import { QuoteGetRequest, QuoteResponse } from "@jup-ag/api"; import { computeLoopingParams, MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; import { ActiveBankInfo, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; -import BigNumber from "bignumber.js"; -import { QuoteGetRequest, QuoteResponse } from "@jup-ag/api"; +import { nativeToUi, TransactionBroadcastType, uiToNative } from "@mrgnlabs/mrgn-common"; import { STATIC_SIMULATION_ERRORS } from "../../errors"; import { ActionMethod } from "../types"; import { closePositionBuilder, loopingBuilder, repayWithCollatBuilder } from "./builders"; import { getSwapQuoteWithRetry } from "../helpers"; -import { nativeToUi, uiToNative } from "@mrgnlabs/mrgn-common"; // ------------------------------------------------------------------// // Helpers // @@ -31,8 +31,8 @@ export async function verifyTxSizeLooping( borrowAmount: BigNumber, quoteResponse: QuoteResponse, connection: Connection, - // isTxnSplit: boolean = false, - priorityFee: number + priorityFee: number, + broadcastType: TransactionBroadcastType ): Promise<{ flashloanTx: VersionedTransaction | null; feedCrankTxs: VersionedTransaction[]; @@ -52,8 +52,8 @@ export async function verifyTxSizeLooping( loopingTxn: null, feedCrankTxs: [], }, - // isTxnSplit, priorityFee, + broadcastType, }); const txCheck = verifyFlashloanTxSize(builder); @@ -134,7 +134,7 @@ export async function verifyTxSizeCollat( quoteResponse: QuoteResponse, connection: Connection, priorityFee: number, - isTxnSplit: boolean = false + broadcastType: TransactionBroadcastType = "BUNDLE" ): Promise<{ flashloanTx: VersionedTransaction | null; feedCrankTxs: VersionedTransaction[]; @@ -156,7 +156,7 @@ export async function verifyTxSizeCollat( feedCrankTxs: [], }, priorityFee, - isTxnSplit, + broadcastType, }); const txCheck = verifyFlashloanTxSize(builder); diff --git a/packages/mrgn-utils/src/actions/individualFlows.ts b/packages/mrgn-utils/src/actions/individualFlows.ts index 370ce89445..a53b727444 100644 --- a/packages/mrgn-utils/src/actions/individualFlows.ts +++ b/packages/mrgn-utils/src/actions/individualFlows.ts @@ -13,7 +13,7 @@ import * as Sentry from "@sentry/nextjs"; import { ExtendedBankInfo, clearAccountCache } from "@mrgnlabs/marginfi-v2-ui-state"; import { MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; -import { Wallet, uiToNative } from "@mrgnlabs/mrgn-common"; +import { TransactionBroadcastType, Wallet, uiToNative } from "@mrgnlabs/mrgn-common"; import { WalletContextStateOverride } from "../wallet"; import { showErrorToast, MultiStepToastHandle } from "../toasts"; @@ -82,6 +82,7 @@ export async function createAccountAndDeposit({ amount, walletContextState, priorityFee, + broadcastType, theme, }: MarginfiActionParams) { if (marginfiClient === null) { @@ -98,7 +99,7 @@ export async function createAccountAndDeposit({ let marginfiAccount: MarginfiAccountWrapper; try { const squadsOptions = await getMaybeSquadsOptions(walletContextState); - marginfiAccount = await marginfiClient.createMarginfiAccount(undefined, squadsOptions); + marginfiAccount = await marginfiClient.createMarginfiAccount(undefined, squadsOptions, priorityFee, broadcastType); clearAccountCache(marginfiClient.provider.publicKey); @@ -118,7 +119,7 @@ export async function createAccountAndDeposit({ } try { - const txnSig = await marginfiAccount.deposit(amount, bank.address, { priorityFeeUi: priorityFee }); + const txnSig = await marginfiAccount.deposit(amount, bank.address, { priorityFeeUi: priorityFee }, broadcastType); multiStepToast.setSuccessAndNext(); return txnSig; } catch (error: any) { @@ -144,6 +145,7 @@ export async function deposit({ amount, priorityFee, actionTxns, + broadcastType, theme, }: MarginfiActionParams) { const multiStepToast = new MultiStepToastHandle("Deposit", [ @@ -157,7 +159,7 @@ export async function deposit({ if (actionTxns?.actionTxn && marginfiClient) { txnSig = await marginfiClient.processTransaction(actionTxns.actionTxn); } else if (marginfiAccount) { - txnSig = await marginfiAccount.deposit(amount, bank.address, { priorityFeeUi: priorityFee }); + txnSig = await marginfiAccount.deposit(amount, bank.address, { priorityFeeUi: priorityFee }, broadcastType); } else { throw new Error("Marginfi account not ready."); } @@ -187,6 +189,7 @@ export async function borrow({ priorityFee, actionTxns, theme, + broadcastType, }: MarginfiActionParams) { const multiStepToast = new MultiStepToastHandle("Borrow", [ { label: `Borrowing ${amount} ${bank.meta.tokenSymbol}` }, @@ -197,9 +200,14 @@ export async function borrow({ try { if (actionTxns?.actionTxn && marginfiClient) { - sigs = await marginfiClient.processTransactions([...actionTxns.additionalTxns, actionTxns.actionTxn]); + sigs = await marginfiClient.processTransactions( + [...actionTxns.additionalTxns, actionTxns.actionTxn], + undefined, + undefined, + broadcastType + ); } else if (marginfiAccount) { - sigs = await marginfiAccount.borrow(amount, bank.address, { priorityFeeUi: priorityFee }); + sigs = await marginfiAccount.borrow(amount, bank.address, { priorityFeeUi: priorityFee }, broadcastType); } else { throw new Error("Marginfi account not ready."); } @@ -229,6 +237,7 @@ export async function withdraw({ priorityFee, actionTxns, theme, + broadcastType, }: MarginfiActionParams) { const multiStepToast = new MultiStepToastHandle("Withdrawal", [ { label: `Withdrawing ${amount} ${bank.meta.tokenSymbol}` }, @@ -239,11 +248,22 @@ export async function withdraw({ try { if (actionTxns?.actionTxn && marginfiClient) { - sigs = await marginfiClient.processTransactions([...actionTxns.additionalTxns, actionTxns.actionTxn]); + sigs = await marginfiClient.processTransactions( + [...actionTxns.additionalTxns, actionTxns.actionTxn], + undefined, + undefined, + broadcastType + ); } else if (marginfiAccount) { - sigs = await marginfiAccount.withdraw(amount, bank.address, bank.isActive && isWholePosition(bank, amount), { - priorityFeeUi: priorityFee, - }); + sigs = await marginfiAccount.withdraw( + amount, + bank.address, + bank.isActive && isWholePosition(bank, amount), + { + priorityFeeUi: priorityFee, + }, + broadcastType + ); } else { throw new Error("Marginfi account not ready."); } @@ -272,6 +292,7 @@ export async function repay({ amount, priorityFee, actionTxns, + broadcastType, theme, }: MarginfiActionParams) { const multiStepToast = new MultiStepToastHandle("Repayment", [ @@ -284,9 +305,15 @@ export async function repay({ if (actionTxns?.actionTxn && marginfiClient) { txnSig = await marginfiClient.processTransaction(actionTxns.actionTxn); } else if (marginfiAccount) { - txnSig = await marginfiAccount.repay(amount, bank.address, bank.isActive && isWholePosition(bank, amount), { - priorityFeeUi: priorityFee, - }); + txnSig = await marginfiAccount.repay( + amount, + bank.address, + bank.isActive && isWholePosition(bank, amount), + { + priorityFeeUi: priorityFee, + }, + broadcastType + ); } else { throw new Error("Marginfi account not ready."); } @@ -316,7 +343,7 @@ export async function looping({ actionTxns, loopingOptions, priorityFee, - isTxnSplit = false, + broadcastType, theme, }: MarginfiActionParams & { isTxnSplit?: boolean }) { if (marginfiClient === null) { @@ -333,11 +360,21 @@ export async function looping({ let sigs: string[] = []; if (actionTxns?.actionTxn) { - sigs = await marginfiClient.processTransactions([...actionTxns.additionalTxns, actionTxns.actionTxn]); + sigs = await marginfiClient.processTransactions( + [...actionTxns.additionalTxns, actionTxns.actionTxn], + undefined, + undefined, + broadcastType + ); } else if (loopingOptions) { console.log("loopingOptions", loopingOptions); if (loopingOptions?.loopingTxn) { - sigs = await marginfiClient.processTransactions([...loopingOptions.feedCrankTxs, loopingOptions.loopingTxn]); + sigs = await marginfiClient.processTransactions( + [...loopingOptions.feedCrankTxs, loopingOptions.loopingTxn], + undefined, + undefined, + broadcastType + ); } else { const { flashloanTx, feedCrankTxs } = await loopingBuilder({ marginfiAccount: marginfiAccount!, @@ -345,8 +382,14 @@ export async function looping({ depositAmount: amount, options: loopingOptions, priorityFee, + broadcastType, }); - sigs = await marginfiClient.processTransactions([...feedCrankTxs, flashloanTx]); + sigs = await marginfiClient.processTransactions( + [...feedCrankTxs, flashloanTx], + undefined, + undefined, + broadcastType + ); } } else { throw new Error("Invalid options provided for looping, please contact support."); @@ -378,7 +421,7 @@ export async function repayWithCollat({ amount, repayWithCollatOptions, priorityFee, - isTxnSplit = false, + broadcastType, theme, actionTxns, }: MarginfiActionParams & { isTxnSplit?: boolean }) { @@ -399,14 +442,21 @@ export async function repayWithCollat({ let sigs: string[] = []; if (actionTxns?.actionTxn) { - sigs = await marginfiClient.processTransactions([...actionTxns.additionalTxns, actionTxns.actionTxn]); + sigs = await marginfiClient.processTransactions( + [...actionTxns.additionalTxns, actionTxns.actionTxn], + undefined, + undefined, + broadcastType + ); } else if (repayWithCollatOptions) { // deprecated if (repayWithCollatOptions.repayCollatTxn) { - sigs = await marginfiClient.processTransactions([ - ...repayWithCollatOptions.feedCrankTxs, - repayWithCollatOptions.repayCollatTxn, - ]); + sigs = await marginfiClient.processTransactions( + [...repayWithCollatOptions.feedCrankTxs, repayWithCollatOptions.repayCollatTxn], + undefined, + undefined, + broadcastType + ); } else { const { flashloanTx, feedCrankTxs } = await repayWithCollatBuilder({ marginfiAccount, @@ -414,10 +464,15 @@ export async function repayWithCollat({ amount, options: repayWithCollatOptions, priorityFee, - isTxnSplit, + broadcastType, }); - sigs = await marginfiClient.processTransactions([...feedCrankTxs, flashloanTx]); + sigs = await marginfiClient.processTransactions( + [...feedCrankTxs, flashloanTx], + undefined, + undefined, + broadcastType + ); } } else { throw new Error("Invalid options provided for repay, please contact support."); @@ -473,11 +528,13 @@ export const closeBalance = async ({ bank, marginfiAccount, priorityFee, + broadcastType, theme, }: { bank: ExtendedBankInfo; marginfiAccount: MarginfiAccountWrapper | null | undefined; priorityFee?: number; + broadcastType?: TransactionBroadcastType; theme?: "light" | "dark"; }) => { if (!marginfiAccount) { @@ -497,9 +554,11 @@ export const closeBalance = async ({ try { let txnSig = ""; if (bank.position.isLending) { - txnSig = (await marginfiAccount.withdraw(0, bank.address, true, { priorityFeeUi: priorityFee })).pop() ?? ""; + txnSig = + (await marginfiAccount.withdraw(0, bank.address, true, { priorityFeeUi: priorityFee }, broadcastType)).pop() ?? + ""; } else { - txnSig = await marginfiAccount.repay(0, bank.address, true, { priorityFeeUi: priorityFee }); + txnSig = await marginfiAccount.repay(0, bank.address, true, { priorityFeeUi: priorityFee }, broadcastType); } multiStepToast.setSuccessAndNext(); return txnSig; diff --git a/packages/mrgn-utils/src/actions/types.ts b/packages/mrgn-utils/src/actions/types.ts index 69d48e9876..ce487921bc 100644 --- a/packages/mrgn-utils/src/actions/types.ts +++ b/packages/mrgn-utils/src/actions/types.ts @@ -6,7 +6,7 @@ import { Connection, PublicKey, Transaction, VersionedTransaction } from "@solan import * as solanaStakePool from "@solana/spl-stake-pool"; import BigNumber from "bignumber.js"; -import { Wallet } from "@mrgnlabs/mrgn-common"; +import { TransactionBroadcastType, Wallet } from "@mrgnlabs/mrgn-common"; import { MarginfiAccountWrapper, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; import { ActionType, ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; @@ -112,7 +112,8 @@ export type MarginfiActionParams = { repayWithCollatOptions?: RepayWithCollatOptions; // deprecated loopingOptions?: LoopingOptions; // deprecated walletContextState?: WalletContextState | WalletContextStateOverride; - priorityFee?: number; + priorityFee: number; + broadcastType: TransactionBroadcastType; theme?: "light" | "dark"; }; diff --git a/packages/mrgn-utils/src/hooks/index.ts b/packages/mrgn-utils/src/hooks/index.ts index 598f983029..ff7addf473 100644 --- a/packages/mrgn-utils/src/hooks/index.ts +++ b/packages/mrgn-utils/src/hooks/index.ts @@ -5,3 +5,4 @@ export * from "./use-ios-version"; export * from "./use-os"; export * from "./use-available-wallets"; export * from "./use-is-mobile"; +export * from "./use-priority-fee"; diff --git a/packages/mrgn-utils/src/hooks/use-priority-fee.ts b/packages/mrgn-utils/src/hooks/use-priority-fee.ts new file mode 100644 index 0000000000..08cbf06ba9 --- /dev/null +++ b/packages/mrgn-utils/src/hooks/use-priority-fee.ts @@ -0,0 +1,61 @@ +import React from "react"; +import { Connection } from "@solana/web3.js"; + +import { MaxCapType, TransactionBroadcastType, TransactionPriorityType } from "@mrgnlabs/mrgn-common"; + +import { usePrevious } from "../mrgnUtils"; +import { fetchPriorityFee } from "../priority.utils"; + +export const usePriorityFee = ( + priorityType: TransactionPriorityType, + broadcastType: TransactionBroadcastType, + maxCapType: MaxCapType, + maxCap: number, + connection?: Connection +): number => { + const prevPriorityType = usePrevious(priorityType); + const prevBroadcastType = usePrevious(broadcastType); + const prevMaxCap = usePrevious(maxCap); + const prevMaxCapType = usePrevious(maxCapType); + const [priorityFee, setPriorityFee] = React.useState(0); + + const calculatePriorityFeeUi = React.useCallback( + async ( + priorityType: TransactionPriorityType, + broadcastType: TransactionBroadcastType, + maxCap: number, + maxCapType: MaxCapType, + connection: Connection + ) => { + const priorityFeeUi = await fetchPriorityFee(maxCapType, maxCap, broadcastType, priorityType, connection); + + setPriorityFee(priorityFeeUi); + }, + [setPriorityFee] + ); + + React.useEffect(() => { + if ( + connection && + (prevPriorityType !== priorityType || + prevBroadcastType !== broadcastType || + prevMaxCap !== maxCap || + prevMaxCapType !== maxCapType) + ) { + calculatePriorityFeeUi(priorityType, broadcastType, maxCap, maxCapType, connection); + } + }, [ + connection, + prevBroadcastType, + priorityType, + prevBroadcastType, + broadcastType, + prevMaxCap, + maxCap, + maxCapType, + prevMaxCapType, + calculatePriorityFeeUi, + ]); + + return priorityFee; +}; diff --git a/packages/mrgn-utils/src/index.ts b/packages/mrgn-utils/src/index.ts index e7ccb4e18a..ea6dec9bc7 100644 --- a/packages/mrgn-utils/src/index.ts +++ b/packages/mrgn-utils/src/index.ts @@ -13,3 +13,4 @@ export * from "./lst-apy.utils"; export * from "./token.utils"; export * from "./jup-referral.utils"; export * from "./bank-data.utils"; +export * from "./priority.utils"; diff --git a/packages/mrgn-utils/src/priority.utils.ts b/packages/mrgn-utils/src/priority.utils.ts new file mode 100644 index 0000000000..741f16bd02 --- /dev/null +++ b/packages/mrgn-utils/src/priority.utils.ts @@ -0,0 +1,158 @@ +import { Connection, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { + getRecentPrioritizationFeesByPercentile, + getCalculatedPrioritizationFeeByPercentile, + MaxCapType, + TransactionBroadcastType, + TransactionPriorityType, +} from "@mrgnlabs/mrgn-common"; + +const enum PriotitizationFeeLevels { + LOW = 2500, + MEDIAN = 5000, + HIGH = 7500, + MAX = 10000, +} + +interface TipFloorDataResponse { + time: string; + landed_tips_25th_percentile: number; + landed_tips_50th_percentile: number; + landed_tips_75th_percentile: number; + landed_tips_95th_percentile: number; + landed_tips_99th_percentile: number; + ema_landed_tips_50th_percentile: number; +} + +export const DEFAULT_MAX_CAP = 0.01; + +export const DEFAULT_PRIORITY_SETTINGS = { + priorityType: "NORMAL" as TransactionPriorityType, + broadcastType: "BUNDLE" as TransactionBroadcastType, + maxCap: DEFAULT_MAX_CAP, + maxCapType: "DYNAMIC" as MaxCapType, +}; + +export const uiToMicroLamports = (ui: number, limitCU: number = 1_400_000) => { + const priorityFeeMicroLamports = ui * LAMPORTS_PER_SOL * 1_000_000; + const microLamports = Math.round(priorityFeeMicroLamports / limitCU); + return microLamports; +}; + +export const microLamportsToUi = (microLamports: number, limitCU: number = 1_400_000) => { + const priorityFeeMicroLamports = microLamports * limitCU; + const priorityFeeUi = priorityFeeMicroLamports / (LAMPORTS_PER_SOL * 1_000_000); + return Math.trunc(priorityFeeUi * LAMPORTS_PER_SOL) / LAMPORTS_PER_SOL; +}; + +export const calculateBundleTipCap = ( + bundleTipData: TipFloorDataResponse, + userMaxCap: number, + multiplier: number = 2 +) => { + const { ema_landed_tips_50th_percentile, landed_tips_95th_percentile } = bundleTipData; + + const maxCap = Math.min(landed_tips_95th_percentile, ema_landed_tips_50th_percentile * multiplier); + + return Math.min(userMaxCap, Math.trunc(maxCap * LAMPORTS_PER_SOL) / LAMPORTS_PER_SOL); +}; + +export const calculatePriorityFeeCap = async (connection: Connection, userMaxCap: number, multiplier: number = 3) => { + const { min, max, mean, median } = await getCalculatedPrioritizationFeeByPercentile( + connection, + { + lockedWritableAccounts: [], // TODO: investigate this + percentile: PriotitizationFeeLevels.MEDIAN, + fallback: false, + }, + 20 + ); + + return Math.min(userMaxCap, microLamportsToUi(Math.max(median * multiplier, max.prioritizationFee))); +}; + +interface TipFloorDataResponse { + time: string; + landed_tips_25th_percentile: number; + landed_tips_50th_percentile: number; + landed_tips_75th_percentile: number; + landed_tips_95th_percentile: number; + landed_tips_99th_percentile: number; + ema_landed_tips_50th_percentile: number; +} + +export const fetchPriorityFee = async ( + maxCapType: MaxCapType, + maxCap: number, + broadcastType: TransactionBroadcastType, + priorityType: TransactionPriorityType, + connection: Connection +) => { + const finalMaxCap = maxCapType === "DYNAMIC" ? DEFAULT_MAX_CAP : maxCap; + if (broadcastType === "BUNDLE") { + return await getBundleTip(priorityType, finalMaxCap); + } else { + return await getRpcPriorityFeeMicroLamports(connection, priorityType, finalMaxCap); + } +}; + +export const getBundleTip = async (priorityType: TransactionPriorityType, userMaxCap: number) => { + const response = await fetch("/api/bundles/tip", { + method: "GET", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch bundle tip"); + } + + const bundleTipData: TipFloorDataResponse = await response.json(); + const maxCap = calculateBundleTipCap(bundleTipData, userMaxCap); + + const { ema_landed_tips_50th_percentile, landed_tips_50th_percentile, landed_tips_75th_percentile } = bundleTipData; + + let priorityFee = 0; + + if (priorityType === "HIGH") { + priorityFee = Math.max(ema_landed_tips_50th_percentile, landed_tips_50th_percentile); + } else if (priorityType === "MAMAS") { + priorityFee = landed_tips_75th_percentile; + } else { + // NORMAL + priorityFee = Math.min(ema_landed_tips_50th_percentile, landed_tips_50th_percentile); + } + + return Math.min(maxCap, Math.trunc(priorityFee * LAMPORTS_PER_SOL) / LAMPORTS_PER_SOL); +}; + +export const getRpcPriorityFeeMicroLamports = async ( + connection: Connection, + priorityType: TransactionPriorityType, + userMaxCap: number +) => { + const { min, max, mean, median } = await getCalculatedPrioritizationFeeByPercentile( + connection, + { + lockedWritableAccounts: [], // TODO: investigate this + percentile: PriotitizationFeeLevels.HIGH, + fallback: false, + }, + 20 + ); + + const maxCap = await calculatePriorityFeeCap(connection, userMaxCap); + + let priorityFee = 0; + + if (priorityType === "HIGH") { + priorityFee = mean; + } else if (priorityType === "MAMAS") { + priorityFee = max.prioritizationFee; + } else { + priorityFee = min.prioritizationFee; + } + + return Math.min(maxCap, priorityFee); +};