Add healthchecks for all candles + Claim ui fee button
This commit is contained in:
142
src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs
Normal file
142
src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './ClaimUiFeesButton'
|
||||
|
||||
244
src/Managing.WebApp/src/hooks/useClaimUiFees.ts
Normal file
244
src/Managing.WebApp/src/hooks/useClaimUiFees.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user