Add healthchecks for all candles + Claim ui fee button

This commit is contained in:
2025-10-29 09:31:00 +07:00
parent 28f2daeb05
commit 29685fd68d
9 changed files with 754 additions and 7 deletions

View File

@@ -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<HealthCheckResult> 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<string, object>();
var isHealthy = true;
var totalChecks = 0;
var healthyChecks = 0;
var degradedChecks = 0;
foreach (var ticker in supportedTickers)
{
var degradedTimeframeResults = new List<Dictionary<string, object>>();
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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["ErrorMessage"] = ex.Message,
["ErrorType"] = ex.GetType().Name
});
}
}
}
}

View File

@@ -4,6 +4,10 @@ using static Managing.Common.Enums;
namespace Managing.Api.HealthChecks
{
/// <summary>
/// Lightweight health check for candle data - only checks ETH across all timeframes
/// For comprehensive checks across all tickers, see CandleDataDetailedHealthCheck
/// </summary>
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);
}
}

View File

@@ -144,6 +144,7 @@ builder.Services.AddHealthChecks()
.AddUrlGroup(new Uri($"{influxUrl}/health"), name: "influxdb", tags: ["database"])
.AddCheck<Web3ProxyHealthCheck>("web3proxy", tags: ["api", "external"])
.AddCheck<CandleDataHealthCheck>("candle-data", tags: ["database", "candles"])
.AddCheck<CandleDataDetailedHealthCheck>("candle-data-detailed", tags: ["database", "candles-detailed"])
.AddCheck<GmxConnectivityHealthCheck>("gmx-connectivity", tags: ["api", "external"])
.AddCheck<OrleansHealthCheck>("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
});

View File

@@ -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",

View File

@@ -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<ClaimUiFeesButtonProps> = ({
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<string | null>(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 (
<div className="flex flex-col gap-2">
<button
className={`btn btn-primary ${isLoading || isConfirming ? 'loading' : ''}`}
onClick={handleClaim}
disabled={isButtonDisabled}
>
{getButtonText()}
</button>
{!isConnected && authenticated && (
<p className="text-sm text-warning">Please connect your wallet to claim UI fees</p>
)}
{!authenticated && (
<p className="text-sm text-warning">Please authenticate to claim UI fees</p>
)}
{(error || localError) && (
<p className="text-sm text-error">{error || localError}</p>
)}
{txHash && (
<div className="text-sm">
<p className="text-info">Transaction Hash:</p>
<a
href={`https://arbiscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="link link-primary break-all"
>
{txHash}
</a>
</div>
)}
{isConnected && address && (
<p className="text-xs text-base-content/70">
Connected: {address.slice(0, 6)}...{address.slice(-4)}
</p>
)}
<div className="text-xs text-base-content/60">
<p className="mb-2">
This will use the GMX SDK to fetch claimable markets and call the ExchangeRouter contract.
</p>
<details className="text-xs">
<summary className="cursor-pointer hover:text-base-content/80">
Filtered tickers ({ALLOWED_TICKERS.length})
</summary>
<div className="mt-2 p-2 bg-base-300 rounded">
{ALLOWED_TICKERS.join(', ')}
</div>
</details>
</div>
</div>
)
}
export default ClaimUiFeesButton

View File

@@ -0,0 +1,2 @@
export { default } from './ClaimUiFeesButton'

View File

@@ -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<string | null>(null)
const [txHash, setTxHash] = useState<string | null>(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,
}
}

View File

@@ -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 <div>Loading...</div>;
@@ -39,7 +89,7 @@ export const Auth = ({ children }: any) => {
if (!authenticated) {
deleteCookie('token')
return (
<div style={{ ...styles }}>
<div style={{ ...styles, flexDirection: 'column', gap: '20px' }}>
<button
onClick={login}
style={{
@@ -54,6 +104,120 @@ export const Auth = ({ children }: any) => {
>
Login with Privy
</button>
{/* Claim UI Fees Section */}
<div style={{ textAlign: 'center' }}>
<button
onClick={() => setShowClaimSection(!showClaimSection)}
style={{
padding: '10px 20px',
backgroundColor: showClaimSection ? '#6B7280' : '#10B981',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '16px'
}}
>
{showClaimSection ? 'Hide' : 'Claim UI Fees'}
</button>
</div>
{showClaimSection && (
<div style={{
padding: '20px',
backgroundColor: '#F3F4F6',
borderRadius: '8px',
maxWidth: '400px',
width: '100%'
}}>
<h3 style={{ margin: '0 0 15px 0', fontSize: '18px', color: '#111827' }}>
Claim GMX UI Fees
</h3>
<p style={{ margin: '0 0 10px 0', fontSize: '14px', color: '#6B7280' }}>
Connect your wallet and enter the account address to claim UI fees from GMX.
</p>
<details style={{ marginBottom: '15px', fontSize: '12px', color: '#6B7280' }}>
<summary style={{ cursor: 'pointer', fontWeight: '500' }}>
Filtered tickers ({ALLOWED_TICKERS.length})
</summary>
<div style={{
marginTop: '8px',
padding: '8px',
backgroundColor: '#E5E7EB',
borderRadius: '4px',
lineHeight: '1.6'
}}>
{ALLOWED_TICKERS.join(', ')}
</div>
</details>
{isConnected && walletAddress && (
<div style={{ marginBottom: '15px', fontSize: '12px', color: '#059669' }}>
Wallet connected: {walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
</div>
)}
{!isConnected && (
<div style={{ marginBottom: '15px', fontSize: '12px', color: '#DC2626' }}>
Please connect your wallet first
</div>
)}
<input
type="text"
placeholder="Enter account address (0x...)"
value={claimAccountAddress}
onChange={(e) => setClaimAccountAddress(e.target.value)}
style={{
width: '100%',
padding: '10px',
marginBottom: '15px',
border: '1px solid #D1D5DB',
borderRadius: '4px',
fontSize: '14px',
boxSizing: 'border-box'
}}
/>
<button
onClick={handleClaimUiFees}
disabled={!isConnected || isClaimingFees || isConfirming || !claimAccountAddress.trim()}
style={{
width: '100%',
padding: '10px 20px',
backgroundColor: (!isConnected || isClaimingFees || isConfirming || !claimAccountAddress.trim()) ? '#9CA3AF' : '#10B981',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: (!isConnected || isClaimingFees || isConfirming || !claimAccountAddress.trim()) ? 'not-allowed' : 'pointer',
fontSize: '16px'
}}
>
{isClaimingFees ? 'Signing...' : isConfirming ? 'Confirming...' : isConfirmed ? 'Claimed!' : 'Claim UI Fees'}
</button>
{(claimError) && (
<div style={{ marginTop: '10px', fontSize: '12px', color: '#DC2626' }}>
{claimError}
</div>
)}
{txHash && (
<div style={{ marginTop: '10px', fontSize: '12px' }}>
<p style={{ color: '#059669', margin: '0 0 5px 0' }}>Transaction Hash:</p>
<a
href={`https://arbiscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#3B82F6', wordBreak: 'break-all' }}
>
{txHash}
</a>
</div>
)}
</div>
)}
</div>
)
} else if (!token) {

View File

@@ -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<GmxClaimableSummary | null>(null)
const [error, setError] = useState<string | null>(null)
const [accountAddress, setAccountAddress] = useState<string>('')
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 && (
<div className="mt-4 p-4 bg-base-200 rounded-lg">
<h4 className="text-lg font-semibold mb-2">Claim UI Fees</h4>
<p className="text-sm mb-4 text-base-content/70">
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.
</p>
<ClaimUiFeesButton
accountAddress={accountAddress}
onSuccess={handleClaimSuccess}
/>
</div>
)}
</div>
)}