From 29685fd68df93f187065ec48a1edd78f747bbfca Mon Sep 17 00:00:00 2001 From: cryptooda Date: Wed, 29 Oct 2025 09:31:00 +0700 Subject: [PATCH] Add healthchecks for all candles + Claim ui fee button --- .../CandleDataDetailedHealthCheck.cs | 142 ++++++++++ .../HealthChecks/CandleDataHealthCheck.cs | 8 +- src/Managing.Api/Program.cs | 8 + src/Managing.WebApp/package.json | 1 + .../ClaimUiFeesButton/ClaimUiFeesButton.tsx | 144 +++++++++++ .../mollecules/ClaimUiFeesButton/index.ts | 2 + .../src/hooks/useClaimUiFees.ts | 244 ++++++++++++++++++ .../src/pages/authPage/auth.tsx | 172 +++++++++++- .../settingsPage/accountFee/accountFee.tsx | 40 ++- 9 files changed, 754 insertions(+), 7 deletions(-) create mode 100644 src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs create mode 100644 src/Managing.WebApp/src/components/mollecules/ClaimUiFeesButton/ClaimUiFeesButton.tsx create mode 100644 src/Managing.WebApp/src/components/mollecules/ClaimUiFeesButton/index.ts create mode 100644 src/Managing.WebApp/src/hooks/useClaimUiFees.ts diff --git a/src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs b/src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs new file mode 100644 index 00000000..b0a142cf --- /dev/null +++ b/src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs @@ -0,0 +1,142 @@ +using Managing.Application.Abstractions.Services; +using Managing.Common; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using static Managing.Common.Enums; + +namespace Managing.Api.HealthChecks +{ + public class CandleDataDetailedHealthCheck : IHealthCheck + { + private readonly IExchangeService _exchangeService; + private readonly TradingExchanges _exchangeToCheck = TradingExchanges.Evm; + + public CandleDataDetailedHealthCheck(IExchangeService exchangeService) + { + _exchangeService = exchangeService; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var now = DateTime.UtcNow; + + // Define timeframes to check with their appropriate start dates and expected freshness + var timeframeChecks = new[] + { + (timeframe: Timeframe.FiveMinutes, startDate: now.AddDays(-1), maxAgeMins: 7), + (timeframe: Timeframe.FifteenMinutes, startDate: now.AddDays(-1), maxAgeMins: 17), + (timeframe: Timeframe.OneHour, startDate: now.AddDays(-3), maxAgeMins: 62), + (timeframe: Timeframe.FourHour, startDate: now.AddDays(-7), maxAgeMins: 242), + (timeframe: Timeframe.OneDay, startDate: now.AddDays(-30), maxAgeMins: 1442) + }; + + var supportedTickers = Constants.GMX.Config.SupportedTickers; + var tickerResults = new Dictionary(); + var isHealthy = true; + var totalChecks = 0; + var healthyChecks = 0; + var degradedChecks = 0; + + foreach (var ticker in supportedTickers) + { + var degradedTimeframeResults = new List>(); + + foreach (var (timeframe, startDate, maxAgeMins) in timeframeChecks) + { + totalChecks++; + + // Fetch candles for this ticker and timeframe + var candles = await _exchangeService.GetCandlesInflux( + _exchangeToCheck, + ticker, + startDate, + timeframe); + + if (candles == null || !candles.Any()) + { + var checkResult = new Dictionary + { + ["Timeframe"] = timeframe.ToString(), + ["StartDate"] = startDate, + ["Status"] = "Degraded", + ["Message"] = $"No candle data found for {ticker} - {timeframe}" + }; + degradedTimeframeResults.Add(checkResult); + isHealthy = false; + degradedChecks++; + } + else + { + var latestCandle = candles.OrderByDescending(c => c.Date).FirstOrDefault(); + var timeDiff = now - latestCandle.Date; + + if (timeDiff.TotalMinutes > maxAgeMins) + { + var checkResult = new Dictionary + { + ["Timeframe"] = timeframe.ToString(), + ["StartDate"] = startDate, + ["LatestCandleDate"] = latestCandle.Date, + ["TimeDifference"] = $"{timeDiff.TotalMinutes:F2} minutes", + ["CandleCount"] = candles.Count(), + ["Status"] = "Degraded", + ["Message"] = $"Data for {ticker} - {timeframe} is outdated. Latest candle is from {latestCandle.Date:yyyy-MM-dd HH:mm:ss} UTC" + }; + degradedTimeframeResults.Add(checkResult); + isHealthy = false; + degradedChecks++; + } + else + { + healthyChecks++; + } + } + } + + // Only add ticker to results if it has degraded checks + if (degradedTimeframeResults.Any()) + { + tickerResults[ticker.ToString()] = degradedTimeframeResults; + } + } + + // Build summary + var summary = new Dictionary + { + ["TotalChecks"] = totalChecks, + ["HealthyChecks"] = healthyChecks, + ["DegradedChecks"] = degradedChecks, + ["CheckedTickers"] = supportedTickers.Select(t => t.ToString()).ToArray(), + ["CheckedTimeframes"] = timeframeChecks.Select(tc => tc.timeframe.ToString()).ToArray(), + ["TickerResults"] = tickerResults + }; + + if (isHealthy) + { + return HealthCheckResult.Healthy( + $"All candle data is up-to-date for {supportedTickers.Length} tickers across {timeframeChecks.Length} timeframes", + data: summary); + } + else + { + return HealthCheckResult.Degraded( + $"{degradedChecks} out of {totalChecks} candle checks are outdated or missing", + data: summary); + } + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy( + "Error checking detailed candle data health", + ex, + data: new Dictionary + { + ["ErrorMessage"] = ex.Message, + ["ErrorType"] = ex.GetType().Name + }); + } + } + } +} + diff --git a/src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs b/src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs index ca4881ab..60407d25 100644 --- a/src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs +++ b/src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs @@ -4,6 +4,10 @@ using static Managing.Common.Enums; namespace Managing.Api.HealthChecks { + /// + /// Lightweight health check for candle data - only checks ETH across all timeframes + /// For comprehensive checks across all tickers, see CandleDataDetailedHealthCheck + /// public class CandleDataHealthCheck : IHealthCheck { private readonly IExchangeService _exchangeService; @@ -90,13 +94,13 @@ namespace Managing.Api.HealthChecks if (isHealthy) { return HealthCheckResult.Healthy( - "All candle timeframes are up-to-date", + $"All {_tickerToCheck} candle timeframes are up-to-date", data: resultsDictionary); } else { return HealthCheckResult.Degraded( - "One or more candle timeframes are outdated or missing", + $"One or more {_tickerToCheck} candle timeframes are outdated or missing", data: resultsDictionary); } } diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs index 8a5dc5af..ffab5f75 100644 --- a/src/Managing.Api/Program.cs +++ b/src/Managing.Api/Program.cs @@ -144,6 +144,7 @@ builder.Services.AddHealthChecks() .AddUrlGroup(new Uri($"{influxUrl}/health"), name: "influxdb", tags: ["database"]) .AddCheck("web3proxy", tags: ["api", "external"]) .AddCheck("candle-data", tags: ["database", "candles"]) + .AddCheck("candle-data-detailed", tags: ["database", "candles-detailed"]) .AddCheck("gmx-connectivity", tags: ["api", "external"]) .AddCheck("orleans-cluster", tags: ["orleans", "cluster"]); @@ -337,6 +338,13 @@ app.UseEndpoints(endpoints => endpoints.MapHealthChecks("/health", new HealthCheckOptions { + Predicate = r => !r.Tags.Contains("candles-detailed"), + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + + endpoints.MapHealthChecks("/health-candles", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("candles-detailed"), ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse }); diff --git a/src/Managing.WebApp/package.json b/src/Managing.WebApp/package.json index f8c4a0e7..6efcd1e1 100644 --- a/src/Managing.WebApp/package.json +++ b/src/Managing.WebApp/package.json @@ -16,6 +16,7 @@ "validate": "./scripts/validate" }, "dependencies": { + "@gmx-io/sdk": "^1.3.1", "@heroicons/react": "^1.0.6", "@microsoft/signalr": "^6.0.5", "@privy-io/react-auth": "^2.7.2", diff --git a/src/Managing.WebApp/src/components/mollecules/ClaimUiFeesButton/ClaimUiFeesButton.tsx b/src/Managing.WebApp/src/components/mollecules/ClaimUiFeesButton/ClaimUiFeesButton.tsx new file mode 100644 index 00000000..152d0a33 --- /dev/null +++ b/src/Managing.WebApp/src/components/mollecules/ClaimUiFeesButton/ClaimUiFeesButton.tsx @@ -0,0 +1,144 @@ +import React, {useState} from 'react' +import {usePrivy} from '@privy-io/react-auth' +import {ALLOWED_TICKERS, useClaimUiFees, useClaimUiFeesTransaction} from '../../../hooks/useClaimUiFees' +import Toast from '../Toast/Toast' + +interface ClaimUiFeesButtonProps { + accountAddress: string + onSuccess?: () => void + disabled?: boolean +} + +/** + * Button component to claim UI fees from GMX + * Automatically fetches markets and tokens from GMX SDK and calls the ExchangeRouter contract + */ +const ClaimUiFeesButton: React.FC = ({ + accountAddress, + onSuccess, + disabled = false, +}) => { + const { authenticated } = usePrivy() + const { claimUiFees, isLoading, error, txHash, isConnected, address } = useClaimUiFees() + const { isConfirming, isConfirmed, isError: txError } = useClaimUiFeesTransaction(txHash) + const [localError, setLocalError] = useState(null) + + const handleClaim = async () => { + if (!authenticated || !isConnected) { + const t = new Toast('Error') + t.update('error', 'Please connect your wallet first') + return + } + + if (!accountAddress) { + const t = new Toast('Error') + t.update('error', 'No account address provided') + return + } + + const toast = new Toast('Claiming UI fees...') + setLocalError(null) + + try { + toast.update('info', 'Fetching claimable markets from GMX SDK...') + + const hash = await claimUiFees(accountAddress) + + toast.update('info', `Transaction submitted: ${hash.slice(0, 10)}...`) + + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred' + setLocalError(errorMessage) + toast.update('error', `Failed to claim UI fees: ${errorMessage}`) + console.error('Error claiming UI fees:', err) + } + } + + // Monitor transaction confirmation + React.useEffect(() => { + if (isConfirmed) { + const t = new Toast('Success') + t.update('success', 'UI fees claimed successfully!') + if (onSuccess) { + onSuccess() + } + } + }, [isConfirmed, onSuccess]) + + React.useEffect(() => { + if (txError) { + const t = new Toast('Error') + t.update('error', 'Transaction failed') + } + }, [txError]) + + const getButtonText = () => { + if (isLoading) return 'Signing...' + if (isConfirming) return 'Confirming...' + if (isConfirmed) return 'Claimed!' + return 'Claim UI Fees' + } + + const isButtonDisabled = disabled || !authenticated || !isConnected || isLoading || isConfirming + + return ( +
+ + + {!isConnected && authenticated && ( +

Please connect your wallet to claim UI fees

+ )} + + {!authenticated && ( +

Please authenticate to claim UI fees

+ )} + + {(error || localError) && ( +

{error || localError}

+ )} + + {txHash && ( +
+

Transaction Hash:

+ + {txHash} + +
+ )} + + {isConnected && address && ( +

+ Connected: {address.slice(0, 6)}...{address.slice(-4)} +

+ )} + +
+

+ This will use the GMX SDK to fetch claimable markets and call the ExchangeRouter contract. +

+
+ + Filtered tickers ({ALLOWED_TICKERS.length}) + +
+ {ALLOWED_TICKERS.join(', ')} +
+
+
+
+ ) +} + +export default ClaimUiFeesButton + diff --git a/src/Managing.WebApp/src/components/mollecules/ClaimUiFeesButton/index.ts b/src/Managing.WebApp/src/components/mollecules/ClaimUiFeesButton/index.ts new file mode 100644 index 00000000..be16e007 --- /dev/null +++ b/src/Managing.WebApp/src/components/mollecules/ClaimUiFeesButton/index.ts @@ -0,0 +1,2 @@ +export { default } from './ClaimUiFeesButton' + diff --git a/src/Managing.WebApp/src/hooks/useClaimUiFees.ts b/src/Managing.WebApp/src/hooks/useClaimUiFees.ts new file mode 100644 index 00000000..ce76c782 --- /dev/null +++ b/src/Managing.WebApp/src/hooks/useClaimUiFees.ts @@ -0,0 +1,244 @@ +import {useState} from 'react' +import {useAccount, useChainId, useSwitchChain, useWaitForTransactionReceipt, useWriteContract} from 'wagmi' +import {arbitrum} from 'viem/chains' +import {parseAbi} from 'viem' +import {GmxSdk} from '@gmx-io/sdk' + +// ExchangeRouter contract address on Arbitrum +const EXCHANGE_ROUTER_ADDRESS = '0x5aC4e27341e4cCcb3e5FD62f9E62db2Adf43dd57' + +// ABI for the claimUiFees function +const CLAIM_UI_FEES_ABI = parseAbi([ + 'function claimUiFees(address[] memory markets, address[] memory tokens, address receiver) external payable returns (uint256[] memory)', +]) + +// GMX SDK Configuration +const GMX_CONFIG = { + chainId: arbitrum.id, + oracleUrl: "https://arbitrum-api.gmxinfra.io", + rpcUrl: "https://arb1.arbitrum.io/rpc", + subsquidUrl: "https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql", +} + +// Allowed tickers for claiming UI fees - matches backend Constants.cs +const ALLOWED_TICKERS = [ + 'BTC', + 'ETH', + 'BNB', + 'DOGE', + 'ADA', + 'SOL', + 'XRP', + 'LINK', + 'RENDER', + 'SUI', + 'GMX', + 'ARB', + 'PEPE', + 'PENDLE', + 'AAVE', + 'HYPE' +] as const + +type AllowedTicker = typeof ALLOWED_TICKERS[number] + +/** + * Maps GMX token symbols to our ticker constants + */ +function normalizeTokenSymbol(symbol: string): string { + // GMX uses WBTC.e, WETH, etc. - normalize to our ticker format + return symbol + .replace('WBTC.e', 'BTC') + .replace('WBTC', 'BTC') + .replace('WETH', 'ETH') + .replace('BTC.b', 'BTC') + .toUpperCase() +} + +/** + * Custom hook to claim UI fees from GMX ExchangeRouter contract + */ +export const useClaimUiFees = () => { + const { address, isConnected } = useAccount() + const chainId = useChainId() + const { switchChainAsync } = useSwitchChain() + const { writeContractAsync } = useWriteContract() + + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [txHash, setTxHash] = useState(null) + + /** + * Fetches market and token addresses for claiming UI fees using GMX SDK directly + * Filters markets based on allowed tickers + */ + const fetchClaimMarkets = async ( + account: string, + allowedTickers: readonly string[] = ALLOWED_TICKERS + ): Promise<{ markets: `0x${string}`[], tokens: `0x${string}`[], filteredCount: number, totalCount: number }> => { + try { + // Initialize GMX SDK + const sdk = new GmxSdk({ + ...GMX_CONFIG, + account: account, + }) + + // Fetch markets info from GMX SDK + const { marketsInfoData } = await sdk.markets.getMarketsInfo() + + if (!marketsInfoData) { + throw new Error('No markets info data available') + } + + console.log('Markets info data:', marketsInfoData) + + const marketAddresses: `0x${string}`[] = [] + const tokenAddresses: `0x${string}`[] = [] + let totalMarkets = 0 + let filteredMarkets = 0 + + // Build claim parameters from markets info + for (const [marketAddress, marketInfo] of Object.entries(marketsInfoData)) { + // Skip swap-only markets (they don't have index tokens for claiming) + if (!marketInfo.indexToken || marketInfo.indexToken.address === '0x0000000000000000000000000000000000000000') { + continue + } + + totalMarkets++ + + // Get the index token symbol and normalize it + const indexTokenSymbol = normalizeTokenSymbol(marketInfo.indexToken.symbol) + + // Filter by allowed tickers + if (!allowedTickers.includes(indexTokenSymbol)) { + console.log(`Skipping market ${marketInfo.name} (${indexTokenSymbol}) - not in allowed tickers`) + continue + } + + filteredMarkets++ + + console.log(`Including market ${marketInfo.name} (${indexTokenSymbol})`) + + // Add market address twice (for long and short positions/tokens) + marketAddresses.push(marketAddress as `0x${string}`, marketAddress as `0x${string}`) + + // Add long token and short token addresses + tokenAddresses.push( + marketInfo.longToken.address as `0x${string}`, + marketInfo.shortToken.address as `0x${string}` + ) + } + + console.log('Fetched claim markets:', { + marketAddresses, + tokenAddresses, + filteredMarkets, + totalMarkets, + allowedTickers + }) + + return { + markets: marketAddresses, + tokens: tokenAddresses, + filteredCount: filteredMarkets, + totalCount: totalMarkets + } + } catch (err) { + console.error('Error fetching claim markets:', err) + throw new Error(`Failed to fetch claim markets: ${err instanceof Error ? err.message : 'Unknown error'}`) + } + } + + /** + * Claims UI fees by calling the ExchangeRouter contract + * This function will automatically fetch the necessary market and token addresses + * @param accountAddress The account address to claim fees for + * @param customTickers Optional custom list of tickers to filter by (defaults to ALLOWED_TICKERS) + */ + const claimUiFees = async (accountAddress: string, customTickers?: readonly string[]) => { + if (!isConnected || !address) { + throw new Error('Wallet not connected') + } + + setIsLoading(true) + setError(null) + setTxHash(null) + + try { + // Switch to Arbitrum if not already on it + if (chainId !== arbitrum.id) { + await switchChainAsync({ chainId: arbitrum.id }) + } + + // Fetch markets and tokens from GMX SDK with ticker filtering + const { markets, tokens, filteredCount, totalCount } = await fetchClaimMarkets( + accountAddress, + customTickers + ) + + if (markets.length === 0 || tokens.length === 0) { + throw new Error(`No claimable UI fees found for the selected tickers (filtered ${filteredCount}/${totalCount} markets)`) + } + + if (markets.length !== tokens.length) { + throw new Error('Markets and tokens arrays must have the same length') + } + + console.log('Claiming UI fees for:') + console.log(`Filtered ${filteredCount}/${totalCount} markets`) + console.log('Markets:', markets) + console.log('Tokens:', tokens) + console.log('Receiver:', address) + + // Call the claimUiFees function on the contract + const hash = await writeContractAsync({ + address: EXCHANGE_ROUTER_ADDRESS, + abi: CLAIM_UI_FEES_ABI, + functionName: 'claimUiFees', + args: [markets, tokens, address], + // The function is payable, but we don't need to send ETH + }) + + setTxHash(hash) + return hash + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred' + setError(errorMessage) + throw new Error(`Failed to claim UI fees: ${errorMessage}`) + } finally { + setIsLoading(false) + } + } + + return { + claimUiFees, + fetchClaimMarkets, + isLoading, + error, + txHash, + isConnected, + address, + allowedTickers: ALLOWED_TICKERS, + } +} + +// Export the allowed tickers for use in other components +export { ALLOWED_TICKERS, type AllowedTicker } + +/** + * Hook to monitor transaction status + */ +export const useClaimUiFeesTransaction = (txHash: string | null) => { + const { data, isLoading, isSuccess, isError } = useWaitForTransactionReceipt({ + hash: txHash as `0x${string}` | undefined, + chainId: arbitrum.id, + }) + + return { + transactionData: data, + isConfirming: isLoading, + isConfirmed: isSuccess, + isError, + } +} + diff --git a/src/Managing.WebApp/src/pages/authPage/auth.tsx b/src/Managing.WebApp/src/pages/authPage/auth.tsx index f6f311c6..fcfa666f 100644 --- a/src/Managing.WebApp/src/pages/authPage/auth.tsx +++ b/src/Managing.WebApp/src/pages/authPage/auth.tsx @@ -5,15 +5,23 @@ import LogIn from '../../components/mollecules/LogIn/LogIn' import useCookie from '../../hooks/useCookie' import {useEffect, useState} from 'react' import {useAuthStore} from '../../app/store/accountStore' +import {ALLOWED_TICKERS, useClaimUiFees, useClaimUiFeesTransaction} from '../../hooks/useClaimUiFees' +import Toast from '../../components/mollecules/Toast/Toast' export const Auth = ({ children }: any) => { const { getCookie, deleteCookie } = useCookie() - const { isConnected } = useAccount() + const { isConnected, address: walletAddress } = useAccount() const { login, ready, authenticated, user } = usePrivy() const token = getCookie('token') const onInitialize = useAuthStore((state) => state.onInitialize) - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true) + const [claimAccountAddress, setClaimAccountAddress] = useState('') + const [showClaimSection, setShowClaimSection] = useState(false) + + // Claim UI fees hook + const { claimUiFees, isLoading: isClaimingFees, txHash, error: claimError } = useClaimUiFees() + const { isConfirming, isConfirmed, isError: txError } = useClaimUiFeesTransaction(txHash) useEffect(() => { if (ready) { @@ -30,7 +38,49 @@ export const Auth = ({ children }: any) => { if (authenticated && token) { onInitialize() } - }, [authenticated, token, onInitialize]); + }, [authenticated, token, onInitialize]) + + // Handle successful claim + useEffect(() => { + if (isConfirmed) { + const toast = new Toast('Success') + toast.update('success', 'UI fees claimed successfully!') + } + }, [isConfirmed]) + + // Handle transaction error + useEffect(() => { + if (txError) { + const toast = new Toast('Error') + toast.update('error', 'Transaction failed') + } + }, [txError]) + + const handleClaimUiFees = async () => { + if (!isConnected) { + const toast = new Toast('Error') + toast.update('error', 'Please connect your wallet first') + return + } + + if (!claimAccountAddress.trim()) { + const toast = new Toast('Error') + toast.update('error', 'Please enter an account address') + return + } + + const toast = new Toast('Claiming UI fees...') + + try { + toast.update('info', 'Fetching claimable markets from GMX SDK...') + await claimUiFees(claimAccountAddress.trim()) + toast.update('info', 'Transaction submitted, waiting for confirmation...') + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred' + toast.update('error', `Failed to claim UI fees: ${errorMessage}`) + console.error('Error claiming UI fees:', err) + } + } if (!ready || isLoading) { return
Loading...
; @@ -39,7 +89,7 @@ export const Auth = ({ children }: any) => { if (!authenticated) { deleteCookie('token') return ( -
+
+ + {/* Claim UI Fees Section */} +
+ +
+ + {showClaimSection && ( +
+

+ Claim GMX UI Fees +

+

+ Connect your wallet and enter the account address to claim UI fees from GMX. +

+
+ + Filtered tickers ({ALLOWED_TICKERS.length}) + +
+ {ALLOWED_TICKERS.join(', ')} +
+
+ + {isConnected && walletAddress && ( +
+ ✓ Wallet connected: {walletAddress.slice(0, 6)}...{walletAddress.slice(-4)} +
+ )} + + {!isConnected && ( +
+ ⚠ Please connect your wallet first +
+ )} + + setClaimAccountAddress(e.target.value)} + style={{ + width: '100%', + padding: '10px', + marginBottom: '15px', + border: '1px solid #D1D5DB', + borderRadius: '4px', + fontSize: '14px', + boxSizing: 'border-box' + }} + /> + + + + {(claimError) && ( +
+ {claimError} +
+ )} + + {txHash && ( +
+

Transaction Hash:

+ + {txHash} + +
+ )} +
+ )}
) } else if (!token) { diff --git a/src/Managing.WebApp/src/pages/settingsPage/accountFee/accountFee.tsx b/src/Managing.WebApp/src/pages/settingsPage/accountFee/accountFee.tsx index 2e66980f..38b3dd78 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/accountFee/accountFee.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/accountFee/accountFee.tsx @@ -2,13 +2,17 @@ import {useMemo, useState} from 'react' import {AccountClient, GmxClaimableSummary} from '../../../generated/ManagingApi' import useApiUrlStore from '../../../app/store/apiStore' import {Table} from '../../../components/mollecules' +import ClaimUiFeesButton from '../../../components/mollecules/ClaimUiFeesButton' +import {usePrivy} from '@privy-io/react-auth' function AccountFee() { const [accountName, setAccountName] = useState('') const [loading, setLoading] = useState(false) const [data, setData] = useState(null) const [error, setError] = useState(null) + const [accountAddress, setAccountAddress] = useState('') const { apiUrl } = useApiUrlStore() + const { authenticated } = usePrivy() const handleFetch = async () => { if (!accountName.trim()) { @@ -18,11 +22,23 @@ function AccountFee() { setLoading(true) setError(null) + setAccountAddress('') try { const accountClient = new AccountClient({}, apiUrl) - const result = await accountClient.account_GetGmxClaimableSummary(accountName.trim()) + + // Fetch both the claimable summary and the account details to get the address + const [result, accountDetails] = await Promise.all([ + accountClient.account_GetGmxClaimableSummary(accountName.trim()), + accountClient.account_GetAccount(accountName.trim()) + ]) + setData(result) + + // Set the account address for claiming + if (accountDetails && accountDetails.key) { + setAccountAddress(accountDetails.key) + } } catch (err: any) { setError(err.message || 'Failed to fetch GMX claimable summary') setData(null) @@ -31,6 +47,13 @@ function AccountFee() { } } + const handleClaimSuccess = () => { + // Refresh the data after successful claim + if (accountName) { + handleFetch() + } + } + // Table columns for all sections const dataColumns = useMemo( () => [ @@ -175,6 +198,21 @@ function AccountFee() { data={uiFeesData} showPagination={false} /> + + {/* Claim UI Fees Button */} + {authenticated && accountAddress && ( +
+

Claim UI Fees

+

+ Connect your wallet and claim UI fees directly from the GMX ExchangeRouter contract. + The markets and tokens will be automatically fetched from the GMX SDK. +

+ +
+ )}
)}