diff --git a/app/markets/components/constants.ts b/app/markets/components/constants.ts index 67ae4cac..63cc9a15 100644 --- a/app/markets/components/constants.ts +++ b/app/markets/components/constants.ts @@ -7,3 +7,24 @@ export enum SortColumn { Borrow = 6, SupplyAPY = 7, } + +// Gas cost to simplify tx flow: do not need to estimate gas for transactions + +export const GAS_COSTS = { + // direct supply through bundler, no approval + BUNDLER_SUPPLY: 180000, + + // An additional supply through the bundler, already approved + SINGLE_SUPPLY: 80000, + + SINGLE_WITHDRAW: 100000, + + // single withdraw + supply + BUNDLER_REBALANCE: 240000, + + // directly borrow from Morpho Blue + DIRECT_WITHDRAW: 100000, +}; + +// additional multiplier for buffer gas. Rabby uses 1.5 +export const GAS_MULTIPLIER = 1.4; diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index d4f0c301..fa918e7d 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -259,11 +259,14 @@ export function RebalanceModal({ setShowProcessModal(true); try { - await executeRebalance(); + const result = await executeRebalance(); // Explicitly refetch AFTER successful execution - refetch(() => { - toast.info('Data refreshed', 'Position data updated after rebalance.'); - }); + + if (result == true) { + refetch(() => { + toast.info('Data refreshed', 'Position data updated after rebalance.'); + }); + } } catch (error) { console.error('Error during rebalance:', error); } finally { diff --git a/app/rewards/components/RewardTable.tsx b/app/rewards/components/RewardTable.tsx index c76615f9..f6aabc30 100644 --- a/app/rewards/components/RewardTable.tsx +++ b/app/rewards/components/RewardTable.tsx @@ -234,6 +234,7 @@ export default function RewardTable({ to: distribution.distributor.address as Address, data: distribution.tx_data as `0x${string}`, chainId: distribution.distributor.chain_id, + // allow estimating gas }); })(); }} diff --git a/src/hooks/useMultiMarketSupply.ts b/src/hooks/useMultiMarketSupply.ts index 9b74dc4c..ce50dd44 100644 --- a/src/hooks/useMultiMarketSupply.ts +++ b/src/hooks/useMultiMarketSupply.ts @@ -9,6 +9,7 @@ import { formatBalance } from '@/utils/balance'; import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; +import { GAS_COSTS, GAS_MULTIPLIER } from 'app/markets/components/constants'; import { useERC20Approval } from './useERC20Approval'; import { useStyledToast } from './useStyledToast'; import { useUserMarketsCache } from './useUserMarketsCache'; @@ -79,6 +80,8 @@ export function useMultiMarketSupply( const txs: `0x${string}`[] = []; + let gas = undefined; + try { // Handle ETH wrapping if needed if (useEth) { @@ -90,7 +93,7 @@ export function useMultiMarketSupply( }), ); } - // Handle token approvals + // Token approvals with Permit 2 else if (usePermit2Setting) { setCurrentStep('signing'); const { sigs, permitSingle } = await signForBundlers(); @@ -120,6 +123,9 @@ export function useMultiMarketSupply( args: [loanAsset.address as Address, totalAmount], }), ); + + const numOfSupplies = supplies.length; + gas = GAS_COSTS.BUNDLER_SUPPLY + GAS_COSTS.SINGLE_SUPPLY * (numOfSupplies - 1); } setCurrentStep('supplying'); @@ -160,6 +166,9 @@ export function useMultiMarketSupply( args: [txs], }) + MONARCH_TX_IDENTIFIER) as `0x${string}`, value: useEth ? totalAmount : 0n, + + // Only add gas for standard approval flow -> skip gas estimation + gas: gas ? BigInt(gas * GAS_MULTIPLIER) : undefined, }); batchAddUserMarkets( diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts index 1c716db2..bcee74b9 100644 --- a/src/hooks/useRebalance.ts +++ b/src/hooks/useRebalance.ts @@ -5,6 +5,7 @@ import morphoBundlerAbi from '@/abis/bundlerV2'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import { GroupedPosition, RebalanceAction } from '@/utils/types'; +import { GAS_COSTS, GAS_MULTIPLIER } from 'app/markets/components/constants'; import { useERC20Approval } from './useERC20Approval'; import { useLocalStorage } from './useLocalStorage'; import { useMorphoBundlerAuthorization } from './useMorphoBundlerAuthorization'; @@ -224,6 +225,8 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () try { const { withdrawTxs, supplyTxs, allMarketKeys } = generateRebalanceTxData(); + let multicallGas = undefined; + if (usePermit2Setting) { // --- Permit2 Flow --- setCurrentStep('approve_permit2'); @@ -280,6 +283,19 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () transactions.push(...withdrawTxs); // Withdraw first transactions.push(erc20TransferTx); // Then transfer assets via standard ERC20 transactions.push(...supplyTxs); // Then supply + + // estimate gas for multicall + multicallGas = GAS_COSTS.BUNDLER_REBALANCE; + + if (supplyTxs.length > 1) { + multicallGas += GAS_COSTS.SINGLE_SUPPLY * (supplyTxs.length - 1); + } + + if (withdrawTxs.length > 1) { + multicallGas += GAS_COSTS.SINGLE_WITHDRAW * (withdrawTxs.length - 1); + } + + console.log('multicallGas', multicallGas); } // Step Final: Execute multicall @@ -295,6 +311,7 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () to: bundlerAddress, data: multicallTx, chainId: groupedPosition.chainId, + gas: multicallGas ? BigInt(multicallGas * GAS_MULTIPLIER) : undefined, }); batchAddUserMarkets( @@ -303,6 +320,8 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () chainId: groupedPosition.chainId, })), ); + + return true; } catch (error) { console.error('Error during rebalance executeRebalance:', error); // Log specific details if available, especially for standard flow issues diff --git a/src/hooks/useSupplyMarket.ts b/src/hooks/useSupplyMarket.ts index d61207e8..e56a2e56 100644 --- a/src/hooks/useSupplyMarket.ts +++ b/src/hooks/useSupplyMarket.ts @@ -11,6 +11,7 @@ import { useUserMarketsCache } from '@/hooks/useUserMarketsCache'; import { formatBalance } from '@/utils/balance'; import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import { Market } from '@/utils/types'; +import { GAS_COSTS, GAS_MULTIPLIER } from 'app/markets/components/constants'; export type SupplyStepType = 'approve' | 'signing' | 'supplying'; @@ -110,6 +111,8 @@ export function useSupplyMarket(market: Market, onSuccess?: () => void): UseSupp try { const txs: `0x${string}`[] = []; + let gas = undefined; + if (useEth) { txs.push( encodeFunctionData({ @@ -146,6 +149,9 @@ export function useSupplyMarket(market: Market, onSuccess?: () => void): UseSupp args: [market.loanAsset.address as Address, supplyAmount], }), ); + + // Standard Flow: add gas + gas = GAS_COSTS.SINGLE_SUPPLY; } setCurrentStep('supplying'); @@ -184,6 +190,9 @@ export function useSupplyMarket(market: Market, onSuccess?: () => void): UseSupp args: [txs], }) + MONARCH_TX_IDENTIFIER) as `0x${string}`, value: useEth ? supplyAmount : 0n, + + // Only add gas for standard approval flow -> skip gas estimation + gas: gas ? BigInt(gas * GAS_MULTIPLIER) : undefined, }); batchAddUserMarkets([ diff --git a/src/hooks/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx index bdd7906f..c34a8465 100644 --- a/src/hooks/useTransactionWithToast.tsx +++ b/src/hooks/useTransactionWithToast.tsx @@ -37,6 +37,8 @@ export function useTransactionWithToast({ hash, }); + console.log('isConfirming', isConfirming); + const onClick = useCallback(() => { if (hash) { // if chainId is not supported, use 1 diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index fcb0ec77..7e1ee3af 100644 --- a/src/hooks/useUserPositionsSummaryData.ts +++ b/src/hooks/useUserPositionsSummaryData.ts @@ -107,7 +107,10 @@ const useUserPositionsSummaryData = (user: string | undefined) => { queryFn: async () => { if (!positions || !user || !blockNums) { console.log('⚠️ [EARNINGS] Missing required data, returning empty earnings'); - return [] as MarketPositionWithEarnings[]; + return { + positions: [] as MarketPositionWithEarnings[], + fetched: false, + }; } console.log('🔄 [EARNINGS] Starting calculation for', positions.length, 'positions'); @@ -144,20 +147,29 @@ const useUserPositionsSummaryData = (user: string | undefined) => { const positionsWithCalculatedEarnings = await Promise.all(positionPromises); console.log('📊 [EARNINGS] All earnings calculations complete'); - return positionsWithCalculatedEarnings; + return { + positions: positionsWithCalculatedEarnings, + fetched: true, + }; }, placeholderData: (prev) => { // If we have positions but no earnings data yet, initialize with empty earnings if (positions?.length) { console.log('📋 [EARNINGS] Using placeholder data with empty earnings'); - return initializePositionsWithEmptyEarnings(positions); + return { + positions: initializePositionsWithEmptyEarnings(positions), + fetched: false, + }; } // If we have previous data, keep it during transitions if (prev) { console.log('📋 [EARNINGS] Keeping previous earnings data during transition'); return prev; } - return [] as MarketPositionWithEarnings[]; + return { + positions: [] as MarketPositionWithEarnings[], + fetched: false, + }; }, enabled: !!positions && !!user && !!blockNums, gcTime: 5 * 60 * 1000, @@ -166,7 +178,8 @@ const useUserPositionsSummaryData = (user: string | undefined) => { // Update hasInitialData when we first get positions with earnings useEffect(() => { - if (positionsWithEarnings && !hasInitialData) { + if (positionsWithEarnings && positionsWithEarnings.fetched && !hasInitialData) { + console.log('positionsWithEarnings', positionsWithEarnings); setHasInitialData(true); } }, [positionsWithEarnings, hasInitialData]); @@ -186,8 +199,7 @@ const useUserPositionsSummaryData = (user: string | undefined) => { // 1. We haven't received initial data yet // 2. Positions are still loading initially // 3. We have positions but no earnings data yet - const isPositionsLoading = - !hasInitialData || positionsLoading || (!!positions?.length && !positionsWithEarnings?.length); + const isPositionsLoading = !hasInitialData || positionsLoading || !positionsWithEarnings?.fetched; // Consider earnings loading if: // 1. Block numbers are loading @@ -196,7 +208,7 @@ const useUserPositionsSummaryData = (user: string | undefined) => { const isEarningsLoading = isLoadingBlockNums || isLoadingEarningsQuery || isFetchingEarnings; return { - positions: positionsWithEarnings ?? [], + positions: positionsWithEarnings?.positions ?? [], isPositionsLoading, isEarningsLoading, isRefetching, diff --git a/src/store/createWagmiConfig.ts b/src/store/createWagmiConfig.ts index 9380af0c..4a5baa8b 100644 --- a/src/store/createWagmiConfig.ts +++ b/src/store/createWagmiConfig.ts @@ -4,12 +4,12 @@ import { rainbowWallet, coinbaseWallet, rabbyWallet, - safeWallet, argentWallet, injectedWallet, trustWallet, ledgerWallet, } from '@rainbow-me/rainbowkit/wallets'; +import { safe } from '@wagmi/connectors'; import { createConfig, http } from 'wagmi'; import { base, mainnet } from 'wagmi/chains'; import { getChainsForEnvironment } from './supportedChains'; @@ -32,7 +32,6 @@ export function createWagmiConfig(projectId: string) { metaMaskWallet, rainbowWallet, coinbaseWallet, - safeWallet, argentWallet, injectedWallet, trustWallet, @@ -53,6 +52,15 @@ export function createWagmiConfig(projectId: string) { [mainnet.id]: http(rpcMainnet), [base.id]: http(rpcBase), }, - connectors, + connectors: [ + ...connectors, + safe({ + shimDisconnect: true, + allowedDomains: [ + // safe global, no localhost + /^(https:\/\/safe\.global|https:\/\/app\.safe\.global)$/, + ], + }), + ], }); }