From 3f34c569688049ab55dbea366f4d0395f6709399 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 13 Jun 2025 14:22:38 +0700 Subject: [PATCH] Get fees to claims --- .../Controllers/AccountController.cs | 14 ++ .../Services/IAccountService.cs | 1 + .../Services}/IWeb3ProxyService.cs | 5 +- .../Accounts/AccountService.cs | 40 +++- .../Accounts/GmxClaimableSummary.cs | 38 ++++ .../Models/Proxy/ClaimingFeesResponse.cs | 40 ++++ .../Services/Web3ProxyService.cs | 40 +++- .../src/plugins/custom/gmx.ts | 34 ++- .../src/routes/api/gmx/index.ts | 92 ++++++++ .../test/plugins/swap-tokens.test.ts | 4 +- .../src/generated/ManagingApi.ts | 82 ++++++++ .../settingsPage/accountFee/accountFee.tsx | 198 ++++++++++++++++++ .../src/pages/settingsPage/settings.tsx | 12 +- 13 files changed, 580 insertions(+), 20 deletions(-) rename src/{Managing.Infrastructure.Web3/Abstractions => Managing.Application.Abstractions/Services}/IWeb3ProxyService.cs (68%) create mode 100644 src/Managing.Domain/Accounts/GmxClaimableSummary.cs create mode 100644 src/Managing.Infrastructure.Web3/Models/Proxy/ClaimingFeesResponse.cs create mode 100644 src/Managing.WebApp/src/pages/settingsPage/accountFee/accountFee.tsx diff --git a/src/Managing.Api/Controllers/AccountController.cs b/src/Managing.Api/Controllers/AccountController.cs index b961a25..97744c6 100644 --- a/src/Managing.Api/Controllers/AccountController.cs +++ b/src/Managing.Api/Controllers/AccountController.cs @@ -75,6 +75,20 @@ namespace Managing.Api.Controllers return Ok(await _AccountService.GetAccountByUser(user, name, true, true)); } + /// + /// Retrieves the GMX claimable fees summary for a specific account. + /// + /// The name of the account to get claimable fees for. + /// The GMX claimable fees summary including funding fees, UI fees, and rebate stats. + [HttpGet] + [Route("{name}/gmx-claimable-summary")] + public async Task> GetGmxClaimableSummary(string name) + { + var user = await GetUser(); + var result = await _AccountService.GetGmxClaimableSummaryAsync(user, name); + return Ok(result); + } + /// /// Deletes a specific account by name for the authenticated user. /// diff --git a/src/Managing.Application.Abstractions/Services/IAccountService.cs b/src/Managing.Application.Abstractions/Services/IAccountService.cs index 5c96632..e26bf67 100644 --- a/src/Managing.Application.Abstractions/Services/IAccountService.cs +++ b/src/Managing.Application.Abstractions/Services/IAccountService.cs @@ -13,4 +13,5 @@ public interface IAccountService Task GetAccountByUser(User user, string name, bool hideSecrets, bool getBalance); Task GetAccountByKey(string key, bool hideSecrets, bool getBalance); IEnumerable GetAccountsBalancesByUser(User user, bool hideSecrets = true); + Task GetGmxClaimableSummaryAsync(User user, string accountName); } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Abstractions/IWeb3ProxyService.cs b/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs similarity index 68% rename from src/Managing.Infrastructure.Web3/Abstractions/IWeb3ProxyService.cs rename to src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs index fca5dfc..9285792 100644 --- a/src/Managing.Infrastructure.Web3/Abstractions/IWeb3ProxyService.cs +++ b/src/Managing.Application.Abstractions/Services/IWeb3ProxyService.cs @@ -1,4 +1,6 @@ -namespace Managing.Infrastructure.Evm.Abstractions +using Managing.Domain.Accounts; + +namespace Managing.Application.Abstractions.Services { public interface IWeb3ProxyService { @@ -6,5 +8,6 @@ namespace Managing.Infrastructure.Evm.Abstractions Task GetPrivyServiceAsync(string endpoint, object payload = null); Task CallGmxServiceAsync(string endpoint, object payload); Task GetGmxServiceAsync(string endpoint, object payload = null); + Task GetGmxClaimableSummaryAsync(string account); } } \ No newline at end of file diff --git a/src/Managing.Application/Accounts/AccountService.cs b/src/Managing.Application/Accounts/AccountService.cs index e090131..3f94dd6 100644 --- a/src/Managing.Application/Accounts/AccountService.cs +++ b/src/Managing.Application/Accounts/AccountService.cs @@ -1,5 +1,4 @@ -using Managing.Application.Abstractions; -using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Common; using Managing.Domain.Accounts; @@ -14,6 +13,7 @@ public class AccountService : IAccountService private readonly IExchangeService _exchangeService; private readonly IEvmManager _evmManager; private readonly ICacheService _cacheService; + private readonly IWeb3ProxyService _web3ProxyService; private readonly ILogger _logger; public AccountService( @@ -21,13 +21,15 @@ public class AccountService : IAccountService ILogger logger, IExchangeService exchangeService, IEvmManager evmManager, - ICacheService cacheService) + ICacheService cacheService, + IWeb3ProxyService web3ProxyService) { _accountRepository = accountRepository; _logger = logger; _exchangeService = exchangeService; _evmManager = evmManager; _cacheService = cacheService; + _web3ProxyService = web3ProxyService; } public async Task CreateAccount(User user, Account request) @@ -161,6 +163,38 @@ public class AccountService : IAccountService return accounts; } + public async Task GetGmxClaimableSummaryAsync(User user, string accountName) + { + // Get the account for the user + var account = await GetAccountByUser(user, accountName, true, false); + + if (account == null) + { + throw new ArgumentException($"Account '{accountName}' not found for user '{user.Name}'"); + } + + // Ensure the account has a valid address/key + if (string.IsNullOrEmpty(account.Key)) + { + throw new ArgumentException($"Account '{accountName}' does not have a valid address"); + } + + try + { + // Call the Web3ProxyService to get GMX claimable summary + var infrastructureResponse = await _web3ProxyService.GetGmxClaimableSummaryAsync(account.Key); + + // Map from infrastructure model to domain model + return infrastructureResponse; + } + catch (Exception ex) when (!(ex is ArgumentException || ex is InvalidOperationException)) + { + _logger.LogError(ex, "Error getting GMX claimable summary for account {AccountName} and user {UserName}", + accountName, user.Name); + throw new InvalidOperationException($"Failed to get GMX claimable summary: {ex.Message}", ex); + } + } + private void ManageProperties(bool hideSecrets, bool getBalance, Account account) { if (account != null) diff --git a/src/Managing.Domain/Accounts/GmxClaimableSummary.cs b/src/Managing.Domain/Accounts/GmxClaimableSummary.cs new file mode 100644 index 0000000..b9de3a1 --- /dev/null +++ b/src/Managing.Domain/Accounts/GmxClaimableSummary.cs @@ -0,0 +1,38 @@ +namespace Managing.Domain.Accounts; + +/// +/// GMX claimable summary data containing funding fees, UI fees, and rebate stats +/// +public class GmxClaimableSummary +{ + public FundingFeesData ClaimableFundingFees { get; set; } + public UiFeesData ClaimableUiFees { get; set; } + public RebateStatsData RebateStats { get; set; } +} + +/// +/// Funding fees claimable data +/// +public class FundingFeesData +{ + public double TotalUsdc { get; set; } +} + +/// +/// UI fees claimable data +/// +public class UiFeesData +{ + public double TotalUsdc { get; set; } +} + +/// +/// Rebate statistics data +/// +public class RebateStatsData +{ + public double TotalRebateUsdc { get; set; } + public double DiscountUsdc { get; set; } + public double RebateFactor { get; set; } + public double DiscountFactor { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Models/Proxy/ClaimingFeesResponse.cs b/src/Managing.Infrastructure.Web3/Models/Proxy/ClaimingFeesResponse.cs new file mode 100644 index 0000000..86716b7 --- /dev/null +++ b/src/Managing.Infrastructure.Web3/Models/Proxy/ClaimingFeesResponse.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace Managing.Infrastructure.Evm.Models.Proxy; + +public class ClaimableFundingFees +{ + [JsonProperty("totalUsdc")] public double TotalUsdc { get; set; } +} + +public class ClaimableUiFees +{ + [JsonProperty("totalUsdc")] public double TotalUsdc { get; set; } +} + +public class Data +{ + [JsonProperty("claimableFundingFees")] public ClaimableFundingFees ClaimableFundingFees { get; set; } + + [JsonProperty("claimableUiFees")] public ClaimableUiFees ClaimableUiFees { get; set; } + + [JsonProperty("rebateStats")] public RebateStats RebateStats { get; set; } +} + +public class RebateStats +{ + [JsonProperty("totalRebateUsdc")] public double TotalRebateUsdc { get; set; } + + [JsonProperty("discountUsdc")] public double DiscountUsdc { get; set; } + + [JsonProperty("rebateFactor")] public double RebateFactor { get; set; } + + [JsonProperty("discountFactor")] public double DiscountFactor { get; set; } +} + +public class ClaimingFeesResponse +{ + [JsonProperty("success")] public bool Success { get; set; } + + [JsonProperty("data")] public Data Data { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs b/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs index 6b8aada..f90028d 100644 --- a/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs +++ b/src/Managing.Infrastructure.Web3/Services/Web3ProxyService.cs @@ -1,9 +1,11 @@ using System.Net.Http.Json; -using Managing.Infrastructure.Evm.Abstractions; -using Microsoft.Extensions.Options; +using System.Text; using System.Text.Json; using System.Web; +using Managing.Application.Abstractions.Services; +using Managing.Domain.Accounts; using Managing.Infrastructure.Evm.Models.Proxy; +using Microsoft.Extensions.Options; namespace Managing.Infrastructure.Evm.Services { @@ -146,6 +148,38 @@ namespace Managing.Infrastructure.Evm.Services } } + public async Task GetGmxClaimableSummaryAsync(string account) + { + var payload = new { account }; + var response = await GetGmxServiceAsync("/claimable-summary", payload); + + + if (response.Data == null) + { + throw new Web3ProxyException("GMX claimable summary data is null"); + } + + // Map from Web3Proxy response model to domain model + return new GmxClaimableSummary + { + ClaimableFundingFees = new FundingFeesData + { + TotalUsdc = response.Data.ClaimableFundingFees.TotalUsdc + }, + ClaimableUiFees = new UiFeesData + { + TotalUsdc = response.Data.ClaimableUiFees.TotalUsdc + }, + RebateStats = new RebateStatsData + { + TotalRebateUsdc = response.Data.RebateStats.TotalRebateUsdc, + DiscountUsdc = response.Data.RebateStats.DiscountUsdc, + RebateFactor = response.Data.RebateStats.RebateFactor, + DiscountFactor = response.Data.RebateStats.DiscountFactor + } + }; + } + private async Task HandleErrorResponse(HttpResponseMessage response) { var statusCode = (int)response.StatusCode; @@ -198,7 +232,7 @@ namespace Managing.Infrastructure.Evm.Services return string.Empty; } - var queryString = new System.Text.StringBuilder("?"); + var queryString = new StringBuilder("?"); bool isFirst = true; foreach (var prop in properties) diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index dc5791a..4cc6377 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -1122,10 +1122,10 @@ export const getClaimableFundingFeesImpl = async ( sdk: GmxSdk ): Promise => { try { - const { marketsInfoData } = await getMarketsInfoWithCache(sdk); + const { marketsInfoData, tokensData } = await getMarketsInfoWithCache(sdk); - if (!marketsInfoData) { - throw new Error("No markets info data available"); + if (!marketsInfoData || !tokensData) { + throw new Error("No markets info data or tokens data available"); } const marketAddresses = Object.keys(marketsInfoData); @@ -1173,7 +1173,7 @@ export const getClaimableFundingFeesImpl = async ( const result = await sdk.executeMulticall(multicallRequest); - // Parse the response + // Parse the response and convert to USD return Object.entries(result.data).reduce((claimableFundingData, [marketAddress, callsResult]: [string, any]) => { const market = marketsInfoData[marketAddress]; @@ -1181,12 +1181,30 @@ export const getClaimableFundingFeesImpl = async ( return claimableFundingData; } - // Get market divisor for proper decimal conversion - const marketDivisor = 1; // You might need to implement getMarketDivisor function + // Get token data for price conversion + const longTokenData = tokensData[market.longToken.address]; + const shortTokenData = tokensData[market.shortToken.address]; + + if (!longTokenData || !shortTokenData) { + console.warn(`Missing token data for market ${marketAddress}`); + return claimableFundingData; + } + + // Convert from wei to token units and then to USD + const longAmount = Number(callsResult.claimableFundingAmountLong.returnValues[0]); + const shortAmount = Number(callsResult.claimableFundingAmountShort.returnValues[0]); + + // Convert from wei to token units using decimals + const longTokenUnits = longAmount / Math.pow(10, longTokenData.decimals); + const shortTokenUnits = shortAmount / Math.pow(10, shortTokenData.decimals); + + // Convert to USD using token prices + const longUsdValue = longTokenUnits * (Number(longTokenData.prices.minPrice) / Math.pow(10, 30)); // GMX prices are in 30 decimals + const shortUsdValue = shortTokenUnits * (Number(shortTokenData.prices.minPrice) / Math.pow(10, 30)); claimableFundingData[marketAddress] = { - claimableFundingAmountLong: Number(callsResult.claimableFundingAmountLong.returnValues[0]) / marketDivisor, - claimableFundingAmountShort: Number(callsResult.claimableFundingAmountShort.returnValues[0]) / marketDivisor, + claimableFundingAmountLong: longUsdValue, + claimableFundingAmountShort: shortUsdValue, }; return claimableFundingData; diff --git a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts index 9ba4995..7ad066f 100644 --- a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts +++ b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts @@ -1,6 +1,11 @@ import {FastifyPluginAsyncTypebox} from '@fastify/type-provider-typebox' import {Type} from '@sinclair/typebox' import {TradeDirection} from '../../../generated/ManagingApiTypes' +import { + getClaimableFundingFeesImpl, + getClaimableUiFeesImpl, + getGmxRebateStatsImpl +} from '../../../plugins/custom/gmx.js' const plugin: FastifyPluginAsyncTypebox = async (fastify) => { // Define route to open a position @@ -149,6 +154,93 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { account ) }) + + // Define route to get all claimable fees and rebate stats + fastify.get('/claimable-summary', { + schema: { + querystring: Type.Object({ + account: Type.String() + }), + response: { + 200: Type.Object({ + success: Type.Boolean(), + data: Type.Optional(Type.Object({ + claimableFundingFees: Type.Object({ + totalUsdc: Type.Number() + }), + claimableUiFees: Type.Object({ + totalUsdc: Type.Number() + }), + rebateStats: Type.Object({ + totalRebateUsdc: Type.Number(), + discountUsdc: Type.Number(), + rebateFactor: Type.Number(), + discountFactor: Type.Number() + }) + })), + error: Type.Optional(Type.String()) + }) + } + } + }, async (request, reply) => { + try { + const { account } = request.query + + // Get GMX client for the account + const sdk = await request.getClientForAddress(account) + + // Call all three implementation functions in parallel + const [fundingFeesData, uiFeesData, rebateStatsData] = await Promise.all([ + getClaimableFundingFeesImpl(sdk), + getClaimableUiFeesImpl(sdk), + getGmxRebateStatsImpl(sdk) + ]) + + // Process funding fees data - only calculate totals + let totalFundingLong = 0 + let totalFundingShort = 0 + + if (fundingFeesData) { + Object.values(fundingFeesData).forEach(marketData => { + totalFundingLong += marketData.claimableFundingAmountLong + totalFundingShort += marketData.claimableFundingAmountShort + }) + } + + // Process UI fees data - only calculate totals + let totalUiFees = 0 + + if (uiFeesData) { + Object.values(uiFeesData).forEach(marketData => { + totalUiFees += marketData.claimableUiFeeAmount + }) + } + + return { + success: true, + data: { + claimableFundingFees: { + totalUsdc: totalFundingLong + totalFundingShort + }, + claimableUiFees: { + totalUsdc: totalUiFees + }, + rebateStats: { + totalRebateUsdc: rebateStatsData?.totalRebateUsd || 0, + discountUsdc: rebateStatsData?.discountUsd || 0, + rebateFactor: rebateStatsData?.rebateFactor || 0, + discountFactor: rebateStatsData?.discountFactor || 0 + } + } + } + } catch (error) { + console.error('Error getting claimable summary:', error) + return { + success: false, + error: `Failed to get claimable summary: ${error instanceof Error ? error.message : 'Unknown error'}` + } + } + }) } export default plugin \ No newline at end of file diff --git a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts index d13f0bb..2f42d68 100644 --- a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts @@ -16,9 +16,9 @@ describe('swap tokens implementation', () => { try { const result = await swapGmxTokensImpl( sdk, - Ticker.GMX, // fromTicker + Ticker.BTC, // fromTicker Ticker.USDC, // toTicker - 2.06 // amount + 0.000056 // amount ) assert.strictEqual(typeof result, 'string') diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 28f6e76..fffde4c 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -209,6 +209,44 @@ export class AccountClient extends AuthorizedApiBase { } return Promise.resolve(null as any); } + + account_GetGmxClaimableSummary(name: string): Promise { + let url_ = this.baseUrl + "/Account/{name}/gmx-claimable-summary"; + if (name === undefined || name === null) + throw new Error("The parameter 'name' must be defined."); + url_ = url_.replace("{name}", encodeURIComponent("" + name)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processAccount_GetGmxClaimableSummary(_response); + }); + } + + protected processAccount_GetGmxClaimableSummary(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as GmxClaimableSummary; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } } export class BacktestClient extends AuthorizedApiBase { @@ -2703,6 +2741,50 @@ export interface Chain { chainId?: number; } +export interface GmxClaimableSummary { + claimableFundingFees?: FundingFeesData | null; + claimableUiFees?: UiFeesData | null; + rebateStats?: RebateStatsData | null; + summary?: SummaryData | null; +} + +export interface FundingFeesData { + totalLongUsdc?: number; + totalShortUsdc?: number; + totalUsdc?: number; + markets?: { [key: string]: FundingFeesMarketData; } | null; +} + +export interface FundingFeesMarketData { + claimableFundingAmountLong?: number; + claimableFundingAmountShort?: number; +} + +export interface UiFeesData { + totalUsdc?: number; + markets?: { [key: string]: UiFeesMarketData; } | null; +} + +export interface UiFeesMarketData { + claimableUiFeeAmount?: number; +} + +export interface RebateStatsData { + totalRebateUsdc?: number; + discountUsdc?: number; + volume?: number; + tier?: number; + rebateFactor?: number; + discountFactor?: number; +} + +export interface SummaryData { + totalClaimableUsdc?: number; + hasFundingFees?: boolean; + hasUiFees?: boolean; + hasRebates?: boolean; +} + export interface Backtest { id: string; finalPnl: number; diff --git a/src/Managing.WebApp/src/pages/settingsPage/accountFee/accountFee.tsx b/src/Managing.WebApp/src/pages/settingsPage/accountFee/accountFee.tsx new file mode 100644 index 0000000..2e66980 --- /dev/null +++ b/src/Managing.WebApp/src/pages/settingsPage/accountFee/accountFee.tsx @@ -0,0 +1,198 @@ +import {useMemo, useState} from 'react' +import {AccountClient, GmxClaimableSummary} from '../../../generated/ManagingApi' +import useApiUrlStore from '../../../app/store/apiStore' +import {Table} from '../../../components/mollecules' + +function AccountFee() { + const [accountName, setAccountName] = useState('') + const [loading, setLoading] = useState(false) + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const { apiUrl } = useApiUrlStore() + + const handleFetch = async () => { + if (!accountName.trim()) { + setError('Please enter an account name') + return + } + + setLoading(true) + setError(null) + + try { + const accountClient = new AccountClient({}, apiUrl) + const result = await accountClient.account_GetGmxClaimableSummary(accountName.trim()) + setData(result) + } catch (err: any) { + setError(err.message || 'Failed to fetch GMX claimable summary') + setData(null) + } finally { + setLoading(false) + } + } + + // Table columns for all sections + const dataColumns = useMemo( + () => [ + { + Header: 'Type', + accessor: 'type', + }, + { + Header: 'Amount (USDC)', + accessor: 'amount', + }, + ], + [] + ) + + // Funding fees table data + const fundingFeesData = useMemo(() => { + if (!data?.claimableFundingFees) return [] + + return [ + { + type: 'Total Funding Fees', + amount: `$${data.claimableFundingFees.totalUsdc?.toFixed(2) || '0.00'}`, + }, + ] + }, [data]) + + // UI fees table data + const uiFeesData = useMemo(() => { + if (!data?.claimableUiFees) return [] + + return [ + { + type: 'Total UI Fees', + amount: `$${data.claimableUiFees.totalUsdc?.toFixed(2) || '0.00'}`, + }, + ] + }, [data]) + + // Rebate stats table data + const rebateStatsData = useMemo(() => { + if (!data?.rebateStats) return [] + + return [ + { + type: 'Total Rebate USDC', + amount: `$${data.rebateStats.totalRebateUsdc?.toFixed(2) || '0.00'}`, + }, + { + type: 'Discount USDC', + amount: `$${data.rebateStats.discountUsdc?.toFixed(2) || '0.00'}`, + }, + { + type: 'Rebate Factor', + amount: (data.rebateStats.rebateFactor || 0).toFixed(4), + }, + { + type: 'Discount Factor', + amount: (data.rebateStats.discountFactor || 0).toFixed(4), + }, + ] + }, [data]) + + // Calculate total claimable for display + const totalClaimable = useMemo(() => { + if (!data) return 0 + const fundingTotal = data.claimableFundingFees?.totalUsdc || 0 + const uiTotal = data.claimableUiFees?.totalUsdc || 0 + return fundingTotal + uiTotal + }, [data]) + + return ( +
+

GMX Account Fees

+ + {/* Input Section */} +
+
+ + setAccountName(e.target.value)} + disabled={loading} + /> +
+ +
+ + {/* Error Display */} + {error && ( +
+
+ + + + {error} +
+
+ )} + + {/* Results Section */} + {data && ( +
+ {/* Total Summary */} +
+
+
Total Claimable
+
${totalClaimable.toFixed(2)}
+
Combined funding fees and UI fees
+
+
+ + {/* Funding Fees Section */} + {data.claimableFundingFees && (data.claimableFundingFees.totalUsdc ?? 0) > 0 && ( +
+

Funding Fees

+ + + )} + + {/* UI Fees Section */} + {data.claimableUiFees && (data.claimableUiFees.totalUsdc ?? 0) > 0 && ( +
+

UI Fees

+
+ + )} + + {/* Rebate Stats Section */} + {data.rebateStats && ((data.rebateStats.totalRebateUsdc ?? 0) > 0 || (data.rebateStats.discountUsdc ?? 0) > 0) && ( +
+

Rebate Statistics

+
+ + )} + + )} + + ) +} + +export default AccountFee \ No newline at end of file diff --git a/src/Managing.WebApp/src/pages/settingsPage/settings.tsx b/src/Managing.WebApp/src/pages/settingsPage/settings.tsx index bf80132..d460f88 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/settings.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/settings.tsx @@ -8,6 +8,7 @@ import MoneyManagementSettings from './moneymanagement/moneyManagement' import Theme from './theme' import DefaultConfig from './defaultConfig/defaultConfig' import UserInfoSettings from './UserInfoSettings' +import AccountFee from './accountFee/accountFee' type TabsType = { label: string @@ -33,18 +34,23 @@ const tabs: TabsType = [ label: 'Account Settings', }, { - Component: Theme, + Component: AccountFee, index: 4, + label: 'Account Fee', + }, + { + Component: Theme, + index: 5, label: 'Theme', }, { Component: DefaultConfig, - index: 5, + index: 6, label: 'Quick Start Config', }, { Component: HealthChecks, - index: 6, + index: 7, label: 'Health Checks', }, ]