From 479fcca6626cdf07335d66f0c2817c7653e51f40 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 14 Nov 2025 18:04:58 +0700 Subject: [PATCH] Add more test for the daily volumes and add button to set the UIFee Factor --- .../PlatformSummaryMetricsTests.cs | 175 +++++++++++++++++ .../src/hooks/useClaimUiFees.ts | 100 ++++++++++ .../src/pages/authPage/auth.tsx | 179 +++++++++++++++++- 3 files changed, 453 insertions(+), 1 deletion(-) diff --git a/src/Managing.Domain.Tests/PlatformSummaryMetricsTests.cs b/src/Managing.Domain.Tests/PlatformSummaryMetricsTests.cs index c1bd62ca..2c5a40b0 100644 --- a/src/Managing.Domain.Tests/PlatformSummaryMetricsTests.cs +++ b/src/Managing.Domain.Tests/PlatformSummaryMetricsTests.cs @@ -800,4 +800,179 @@ public class PlatformSummaryMetricsTests result.PositionCountByAsset.Should().HaveCount(1); result.TotalPlatformVolume.Should().BeGreaterThan(0); } + + [Fact] + public void CalculateDailySnapshotFromPositions_WithDateFiltering_CalculatesCumulativeMetricsCorrectly() + { + // Arrange - Create positions with different dates spanning multiple days + var baseDate = TestDate.Date; + var positions = new List + { + // Day 1: Two positions (one finished, one filled) + CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false), + CreateFilledPosition(40000m, 0.2m, TradeDirection.Short, 1m), + + // Day 2: One finished position + CreateFinishedPosition(60000m, 0.05m, TradeDirection.Long, 2m, 61200m, false), + + // Day 3: Two positions (future dates - should be excluded) + }; + + // Set specific dates for positions + positions[0].Date = baseDate.AddHours(10); // Day 1, 10 AM + positions[1].Date = baseDate.AddHours(14); // Day 1, 2 PM + positions[2].Date = baseDate.AddDays(1).AddHours(11); // Day 2, 11 AM + + // Future positions (should be excluded) + var futurePositions = new List + { + CreateFinishedPosition(30000m, 0.15m, TradeDirection.Short, 1m, 29100m, false), + CreateFilledPosition(70000m, 0.08m, TradeDirection.Long, 1m) + }; + futurePositions[0].Date = baseDate.AddDays(2).AddHours(9); // Day 3, 9 AM + futurePositions[1].Date = baseDate.AddDays(2).AddHours(16); // Day 3, 4 PM + + var allPositions = positions.Concat(futurePositions).ToList(); + + // Act - Calculate snapshot for Day 2 (should include Day 1 and Day 2 positions only) + var targetDate = baseDate.AddDays(1); // Day 2 + var filteredPositions = allPositions.Where(p => p.Date.Date <= targetDate).ToList(); + var metrics = TradingBox.CalculatePlatformSummaryMetrics(filteredPositions); + + // Assert - Should include positions from Day 1 and Day 2 only + metrics.TotalLifetimePositionCount.Should().Be(3); // 2 from Day 1 + 1 from Day 2 + + // Calculate expected volume: Day 1 positions + Day 2 position + // Default takeProfitPercentage is 0.04m (4%) + var day1Volume = (50000m * 0.1m * 1m + 52000m * 0.1m * 1m) + // Finished long: 5000 + 5200 = 10200 + (40000m * 0.2m * 1m); // Open short: 8000 (not finished, no close volume) + var day2Volume = (60000m * 0.05m * 2m + 62400m * 0.05m * 2m); // Finished long with 2x leverage: 6000 + 6240 = 12240 + var expectedTotalVolume = day1Volume + day2Volume; // 10200 + 8000 + 12240 = 30440 + + metrics.TotalPlatformVolume.Should().Be(expectedTotalVolume); + metrics.OpenInterest.Should().Be(40000m * 0.2m * 1m); // Only the open short position from Day 1 + + // Should have both BTC and position counts + metrics.VolumeByAsset.Should().ContainKey("BTC"); + metrics.PositionCountByAsset["BTC"].Should().Be(3); + } + + [Fact] + public void CalculateDailySnapshotFromPositions_WithNoPositionsForDate_ReturnsEmptyMetrics() + { + // Arrange - Create positions all after the target date + var baseDate = TestDate.Date; + var positions = new List + { + CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false), + CreateFilledPosition(40000m, 0.2m, TradeDirection.Short, 1m) + }; + + // Set dates after target date + positions[0].Date = baseDate.AddDays(1).AddHours(10); + positions[1].Date = baseDate.AddDays(2).AddHours(14); + + // Act - Calculate snapshot for a date before all positions + var targetDate = baseDate; // Today, before all positions + var filteredPositions = positions.Where(p => p.Date.Date <= targetDate).ToList(); + var metrics = TradingBox.CalculatePlatformSummaryMetrics(filteredPositions); + + // Assert - Should have no positions for this date + metrics.TotalLifetimePositionCount.Should().Be(0); + metrics.TotalPlatformVolume.Should().Be(0); + metrics.TotalPlatformFees.Should().Be(0); + metrics.TotalPlatformPnL.Should().Be(0); + metrics.NetPnL.Should().Be(0); + metrics.OpenInterest.Should().Be(0); + metrics.VolumeByAsset.Should().BeEmpty(); + metrics.PositionCountByAsset.Should().BeEmpty(); + metrics.PositionCountByDirection.Should().BeEmpty(); + } + + [Fact] + public void CalculateDailySnapshotFromPositions_WithMixedAssetsAndDirections_CalculatesAssetBreakdownsCorrectly() + { + // Arrange - Create positions with different assets and directions over multiple days + var baseDate = TestDate.Date; + var positions = new List + { + // Day 1: BTC Long (finished) and ETH Short (open) + CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 51000m, false), // BTC finished + CreateFilledPosition(3000m, 1m, TradeDirection.Short, 1m), // ETH open + + // Day 2: BTC Short (finished) and ETH Long (finished) + CreateFinishedPosition(40000m, 0.2m, TradeDirection.Short, 2m, 39200m, false), // BTC finished + CreateFinishedPosition(3100m, 0.5m, TradeDirection.Long, 1m, 3180m, false) // ETH finished + }; + + // Set dates + positions[0].Date = baseDate.AddHours(10); // Day 1 BTC + positions[1].Date = baseDate.AddHours(14); // Day 1 ETH + positions[2].Date = baseDate.AddDays(1).AddHours(11); // Day 2 BTC + positions[3].Date = baseDate.AddDays(1).AddHours(15); // Day 2 ETH + + // Set ETH tickers for ETH positions + positions[1].Ticker = Ticker.ETH; + positions[3].Ticker = Ticker.ETH; + + // Act - Calculate snapshot for Day 2 (includes all positions) + var targetDate = baseDate.AddDays(1); + var filteredPositions = positions.Where(p => p.Date.Date <= targetDate).ToList(); + var metrics = TradingBox.CalculatePlatformSummaryMetrics(filteredPositions); + + // Assert - Should include all 4 positions + metrics.TotalLifetimePositionCount.Should().Be(4); + metrics.VolumeByAsset.Should().HaveCount(2); // BTC and ETH + metrics.PositionCountByAsset.Should().HaveCount(2); + metrics.PositionCountByAsset["BTC"].Should().Be(2); + metrics.PositionCountByAsset["ETH"].Should().Be(2); + + // Should have only one direction (one open short ETH, rest are finished) + metrics.PositionCountByDirection.Should().HaveCount(1); + metrics.PositionCountByDirection[TradeDirection.Short].Should().Be(1); // Only the open ETH short position + + // Open interest should only include the open ETH short position + metrics.OpenInterest.Should().Be(3000m * 1m * 1m); // ETH open short: 3000 + } + + [Fact] + public void CalculateDailySnapshotFromPositions_WithPositionSpanningMultipleDays_CalculatesVolumePerDayCorrectly() + { + // Arrange - Create positions representing the same position at different stages + var baseDate = TestDate.Date; + + // Position as it appears on Day 1 (still open) + var positionDay1 = CreateFilledPosition(50000m, 0.1m, TradeDirection.Long, 1m); + positionDay1.Date = baseDate.AddHours(12); // Day 1, noon + + // Position as it appears on Day 2 (now closed) + var positionDay2 = CreateFinishedPosition(50000m, 0.1m, TradeDirection.Long, 1m, 52000m, false); + positionDay2.Date = baseDate.AddHours(12); // Same open date + positionDay2.TakeProfit1.Date = baseDate.AddDays(1).AddHours(10); // Closed on Day 2 + + // Act - Calculate snapshot for Day 1 (position opened but not closed yet) + var day1Positions = new List { positionDay1 }; + var day1Metrics = TradingBox.CalculatePlatformSummaryMetrics(day1Positions); + + // Calculate snapshot for Day 2 (position opened and closed) + var day2Positions = new List { positionDay2 }; + var day2Metrics = TradingBox.CalculatePlatformSummaryMetrics(day2Positions); + + // Assert - Day 1: Only opening volume (position still open) + var expectedDay1Volume = 50000m * 0.1m * 1m; // Open: 5000 + day1Metrics.TotalPlatformVolume.Should().Be(expectedDay1Volume); + day1Metrics.TotalLifetimePositionCount.Should().Be(1); + day1Metrics.OpenInterest.Should().Be(expectedDay1Volume); // Position still open + + // Assert - Day 2: Opening volume + closing volume (position now closed) + var expectedDay2Volume = 50000m * 0.1m * 1m + 52000m * 0.1m * 1m; // Open: 5000 + Close: 5200 = 10200 + day2Metrics.TotalPlatformVolume.Should().Be(expectedDay2Volume); + day2Metrics.TotalLifetimePositionCount.Should().Be(1); + day2Metrics.OpenInterest.Should().Be(0m); // Position now closed + + // Assert - Volume increased from Day 1 to Day 2 (closing volume added) + day2Metrics.TotalPlatformVolume.Should().BeGreaterThan(day1Metrics.TotalPlatformVolume); + var volumeIncrease = day2Metrics.TotalPlatformVolume - day1Metrics.TotalPlatformVolume; + volumeIncrease.Should().Be(5200m); // The closing volume added on Day 2 + } } diff --git a/src/Managing.WebApp/src/hooks/useClaimUiFees.ts b/src/Managing.WebApp/src/hooks/useClaimUiFees.ts index 5c4232a9..9c4881ff 100644 --- a/src/Managing.WebApp/src/hooks/useClaimUiFees.ts +++ b/src/Managing.WebApp/src/hooks/useClaimUiFees.ts @@ -222,6 +222,106 @@ export const useClaimUiFees = () => { } } +// ABI for the setUiFeeFactor function +const SET_UI_FEE_FACTOR_ABI = parseAbi([ + 'function setUiFeeFactor(uint256 uiFeeFactor) external', +]) + +// UI Fee Receiver contract address on Arbitrum (from Arbiscan) +const UI_FEE_RECEIVER_ADDRESS = '0x602b805EedddBbD9ddff44A7dcBD46cb07849685' + +/** + * Custom hook to update UI fee factor on GMX ExchangeRouter contract + */ +export const useUpdateUiFee = () => { + 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) + + /** + * Updates the UI fee factor by calling the ExchangeRouter contract + * @param percentage The fee percentage (e.g., 0.1 for 0.1%) + */ + const updateUiFeeFactor = async (percentage: number) => { + if (!isConnected || !address) { + throw new Error('Wallet not connected') + } + + if (percentage < 0 || percentage > 100) { + throw new Error('Fee percentage must be between 0 and 100') + } + + setIsLoading(true) + setError(null) + setTxHash(null) + + try { + // Switch to Arbitrum if not already on it + if (chainId !== arbitrum.id) { + await switchChainAsync({ chainId: arbitrum.id }) + } + + // Convert percentage to basis points (1% = 100 basis points) + // GMX typically uses factors with 30 decimal places (10^30 total precision) + const basisPoints = Math.round(percentage * 100) // Convert to basis points + const feeFactor = BigInt(basisPoints) * BigInt(10) ** BigInt(26) // Multiply by 10^26 to get the factor (10^30 - 10^4 for basis points) + + console.log('Updating UI fee factor:') + console.log(`Percentage: ${percentage}%`) + console.log(`Basis points: ${basisPoints}`) + console.log(`Fee factor: ${feeFactor.toString()}`) + + // Call the setUiFeeFactor function on the contract + const hash = await writeContractAsync({ + address: UI_FEE_RECEIVER_ADDRESS, + abi: SET_UI_FEE_FACTOR_ABI, + functionName: 'setUiFeeFactor', + args: [feeFactor], + }) + + setTxHash(hash) + return hash + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred' + setError(errorMessage) + throw new Error(`Failed to update UI fee factor: ${errorMessage}`) + } finally { + setIsLoading(false) + } + } + + return { + updateUiFeeFactor, + isLoading, + error, + txHash, + isConnected, + address, + } +} + +/** + * Hook to monitor UI fee update transaction status + */ +export const useUpdateUiFeeTransaction = (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, + } +} + // Export the allowed tickers for use in other components export { ALLOWED_TICKERS, type AllowedTicker } diff --git a/src/Managing.WebApp/src/pages/authPage/auth.tsx b/src/Managing.WebApp/src/pages/authPage/auth.tsx index c2db0b4f..3206bfba 100644 --- a/src/Managing.WebApp/src/pages/authPage/auth.tsx +++ b/src/Managing.WebApp/src/pages/authPage/auth.tsx @@ -5,7 +5,13 @@ 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 { + ALLOWED_TICKERS, + useClaimUiFees, + useClaimUiFeesTransaction, + useUpdateUiFee, + useUpdateUiFeeTransaction +} from '../../hooks/useClaimUiFees' import Toast from '../../components/mollecules/Toast/Toast' import useApiUrlStore from '../../app/store/apiStore' @@ -85,11 +91,19 @@ export const Auth = ({ children }: any) => { const [isLoading, setIsLoading] = useState(true) const [claimAccountAddress, setClaimAccountAddress] = useState('') const [showClaimSection, setShowClaimSection] = useState(false) + + // Update UI Fee state + const [uiFeePercentage, setUiFeePercentage] = useState('') + const [showUpdateUiFeeSection, setShowUpdateUiFeeSection] = useState(false) // Claim UI fees hook const { claimUiFees, isLoading: isClaimingFees, txHash, error: claimError } = useClaimUiFees() const { isConfirming, isConfirmed, isError: txError } = useClaimUiFeesTransaction(txHash) + // Update UI fee hook + const { updateUiFeeFactor, isLoading: isUpdatingFee, txHash: updateTxHash, error: updateFeeError } = useUpdateUiFee() + const { isConfirming: isUpdateConfirming, isConfirmed: isUpdateConfirmed, isError: updateTxError } = useUpdateUiFeeTransaction(updateTxHash) + useEffect(() => { if (ready) { const timeout = setTimeout(() => { @@ -122,6 +136,23 @@ export const Auth = ({ children }: any) => { toast.update('error', 'Transaction failed') } }, [txError]) + + // Handle successful UI fee update + useEffect(() => { + if (isUpdateConfirmed) { + const toast = new Toast('Success') + toast.update('success', 'UI fee factor updated successfully!') + setUiFeePercentage('') // Clear the input + } + }, [isUpdateConfirmed]) + + // Handle UI fee update transaction error + useEffect(() => { + if (updateTxError) { + const toast = new Toast('Error') + toast.update('error', 'UI fee update transaction failed') + } + }, [updateTxError]) const handleClaimUiFees = async () => { if (!isConnected) { @@ -149,6 +180,33 @@ export const Auth = ({ children }: any) => { } } + const handleUpdateUiFee = async () => { + if (!isConnected) { + const toast = new Toast('Error') + toast.update('error', 'Please connect your wallet first') + return + } + + const percentage = parseFloat(uiFeePercentage) + if (isNaN(percentage) || percentage < 0 || percentage > 100) { + const toast = new Toast('Error') + toast.update('error', 'Please enter a valid percentage between 0 and 100') + return + } + + const toast = new Toast('Updating UI fee factor...') + + try { + toast.update('info', 'Submitting transaction to update UI fee factor...') + await updateUiFeeFactor(percentage) + 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 update UI fee factor: ${errorMessage}`) + console.error('Error updating UI fee factor:', err) + } + } + if (!ready || isLoading) { return
Loading...
; } @@ -288,6 +346,125 @@ export const Auth = ({ children }: any) => { )} )} + + {/* Update UI Fee Section */} +
+ +
+ + {showUpdateUiFeeSection && ( +
+

+ Update GMX UI Fee Factor +

+

+ Set the UI fee factor percentage for GMX trading. This will be automatically formatted for the smart contract. +

+
+ + How it works + +
+ Enter a percentage (e.g., 0.1 for 0.1%). This gets converted to basis points and formatted for the GMX contract. + The fee factor determines the percentage of trading fees that go to the UI/interface. +
+
+ + {isConnected && walletAddress && ( +
+ ✓ Wallet connected: {walletAddress.slice(0, 6)}...{walletAddress.slice(-4)} +
+ )} + + {!isConnected && ( +
+ ⚠ Please connect your wallet first +
+ )} + + setUiFeePercentage(e.target.value)} + min="0" + max="100" + step="0.01" + style={{ + width: '100%', + padding: '10px', + marginBottom: '15px', + border: '1px solid #D1D5DB', + borderRadius: '4px', + fontSize: '14px', + boxSizing: 'border-box' + }} + /> + + + + {(updateFeeError) && ( +
+ {updateFeeError} +
+ )} + + {updateTxHash && ( +
+

Transaction Hash:

+ + {updateTxHash} + +
+ )} +
+ )} ) } else if (!token) {