Add ETH and USDC balance check before start/restart bot and autoswap
This commit is contained in:
@@ -14,8 +14,8 @@
|
|||||||
},
|
},
|
||||||
"Web3Proxy": {
|
"Web3Proxy": {
|
||||||
"BaseUrl": "http://srv-captain--web3-proxy:4111",
|
"BaseUrl": "http://srv-captain--web3-proxy:4111",
|
||||||
"MaxRetryAttempts": 3,
|
"MaxRetryAttempts": 2,
|
||||||
"RetryDelayMs": 1000,
|
"RetryDelayMs": 1500,
|
||||||
"TimeoutSeconds": 30
|
"TimeoutSeconds": 30
|
||||||
},
|
},
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
},
|
},
|
||||||
"Web3Proxy": {
|
"Web3Proxy": {
|
||||||
"BaseUrl": "http://srv-captain--web3-proxy:4111",
|
"BaseUrl": "http://srv-captain--web3-proxy:4111",
|
||||||
"MaxRetryAttempts": 3,
|
"MaxRetryAttempts": 2,
|
||||||
"RetryDelayMs": 1000,
|
"RetryDelayMs": 1500,
|
||||||
"TimeoutSeconds": 30
|
"TimeoutSeconds": 30
|
||||||
},
|
},
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
},
|
},
|
||||||
"Web3Proxy": {
|
"Web3Proxy": {
|
||||||
"BaseUrl": "http://localhost:4111",
|
"BaseUrl": "http://localhost:4111",
|
||||||
"MaxRetryAttempts": 3,
|
"MaxRetryAttempts": 2,
|
||||||
"RetryDelayMs": 1000,
|
"RetryDelayMs": 1500,
|
||||||
"TimeoutSeconds": 30
|
"TimeoutSeconds": 30
|
||||||
},
|
},
|
||||||
"Kaigen": {
|
"Kaigen": {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public interface IExchangeService
|
|||||||
decimal GetVolume(Account account, Ticker ticker);
|
decimal GetVolume(Account account, Ticker ticker);
|
||||||
Task<List<Trade>> GetTrades(Account account, Ticker ticker);
|
Task<List<Trade>> GetTrades(Account account, Ticker ticker);
|
||||||
Task<bool> CancelOrder(Account account, Ticker ticker);
|
Task<bool> CancelOrder(Account account, Ticker ticker);
|
||||||
decimal GetFee(Account account, bool isForPaperTrading = false);
|
Task<decimal> GetFee(Account account, bool isForPaperTrading = false);
|
||||||
Task<Candle> GetCandle(Account account, Ticker ticker, DateTime date);
|
Task<Candle> GetCandle(Account account, Ticker ticker, DateTime date);
|
||||||
Task<decimal> GetQuantityInPosition(Account account, Ticker ticker);
|
Task<decimal> GetQuantityInPosition(Account account, Ticker ticker);
|
||||||
|
|
||||||
|
|||||||
@@ -17,5 +17,7 @@ namespace Managing.Application.Abstractions.Services
|
|||||||
Task<SwapInfos> SendTokenAsync(string senderAddress, string recipientAddress, Ticker ticker, decimal amount, int? chainId = null);
|
Task<SwapInfos> SendTokenAsync(string senderAddress, string recipientAddress, Ticker ticker, decimal amount, int? chainId = null);
|
||||||
|
|
||||||
Task<List<Balance>> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains);
|
Task<List<Balance>> GetWalletBalanceAsync(string address, Ticker[] assets, string[] chains);
|
||||||
|
|
||||||
|
Task<decimal> GetEstimatedGasFeeUsdAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Managing.Application.Bots.Models;
|
||||||
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
using Managing.Domain.Trades;
|
using Managing.Domain.Trades;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
@@ -44,4 +46,11 @@ public interface IBotService
|
|||||||
string? agentName = null,
|
string? agentName = null,
|
||||||
string sortBy = "CreateDate",
|
string sortBy = "CreateDate",
|
||||||
string sortDirection = "Desc");
|
string sortDirection = "Desc");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks USDC and ETH balances for EVM/GMX V2 accounts
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="account">The account to check balances for</param>
|
||||||
|
/// <returns>Balance check result</returns>
|
||||||
|
Task<BalanceCheckResult> CheckAccountBalancesAsync(Account account);
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using Managing.Application.Abstractions.Services;
|
|||||||
using Managing.Application.Bots.Models;
|
using Managing.Application.Bots.Models;
|
||||||
using Managing.Application.Orleans;
|
using Managing.Application.Orleans;
|
||||||
using Managing.Common;
|
using Managing.Common;
|
||||||
|
using Managing.Core.Exceptions;
|
||||||
using Managing.Domain.Statistics;
|
using Managing.Domain.Statistics;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
@@ -236,8 +237,6 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BalanceCheckResult> CheckAndEnsureEthBalanceAsync(Guid requestingBotId, string accountName)
|
public async Task<BalanceCheckResult> CheckAndEnsureEthBalanceAsync(Guid requestingBotId, string accountName)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
// Check if a swap is already in progress
|
// Check if a swap is already in progress
|
||||||
if (_state.State.IsSwapInProgress)
|
if (_state.State.IsSwapInProgress)
|
||||||
@@ -306,17 +305,28 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If ETH balance is sufficient, return success
|
// If ETH balance is sufficient, return success
|
||||||
if (balanceData.EthValueInUsd >= Constants.GMX.Config.MinimumEthBalance)
|
if (balanceData.EthValueInUsd >= Constants.GMX.Config.MinimumTradeEthBalanceUsd)
|
||||||
{
|
{
|
||||||
return new BalanceCheckResult
|
return new BalanceCheckResult
|
||||||
{
|
{
|
||||||
IsSuccessful = true,
|
IsSuccessful = true,
|
||||||
FailureReason = BalanceCheckFailureReason.None,
|
FailureReason = BalanceCheckFailureReason.None,
|
||||||
Message = "Balance check successful",
|
Message = "Balance check successful - Enough ETH balance for trading",
|
||||||
ShouldStopBot = false
|
ShouldStopBot = false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (balanceData.EthValueInUsd < Constants.GMX.Config.MinimumSwapEthBalanceUsd)
|
||||||
|
{
|
||||||
|
return new BalanceCheckResult
|
||||||
|
{
|
||||||
|
IsSuccessful = false,
|
||||||
|
FailureReason = BalanceCheckFailureReason.InsufficientEthBelowMinimum,
|
||||||
|
Message = "ETH balance below minimum required amount",
|
||||||
|
ShouldStopBot = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we have enough USDC for swap (need at least 5 USD for swap)
|
// Check if we have enough USDC for swap (need at least 5 USD for swap)
|
||||||
if (balanceData.UsdcValue <
|
if (balanceData.UsdcValue <
|
||||||
(Constants.GMX.Config.MinimumPositionAmount + (decimal)Constants.GMX.Config.AutoSwapAmount))
|
(Constants.GMX.Config.MinimumPositionAmount + (decimal)Constants.GMX.Config.AutoSwapAmount))
|
||||||
@@ -345,17 +355,6 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
// Get user for the swap
|
// Get user for the swap
|
||||||
var userId = (int)this.GetPrimaryKeyLong();
|
var userId = (int)this.GetPrimaryKeyLong();
|
||||||
var user = await _userService.GetUserByIdAsync(userId);
|
var user = await _userService.GetUserByIdAsync(userId);
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
_logger.LogError("User {UserId} not found for swap operation", userId);
|
|
||||||
return new BalanceCheckResult
|
|
||||||
{
|
|
||||||
IsSuccessful = false,
|
|
||||||
FailureReason = BalanceCheckFailureReason.BalanceFetchError,
|
|
||||||
Message = "User not found for swap operation",
|
|
||||||
ShouldStopBot = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the swap
|
// Perform the swap
|
||||||
var swapInfo = await _accountService.SwapGmxTokensAsync(user, accountName,
|
var swapInfo = await _accountService.SwapGmxTokensAsync(user, accountName,
|
||||||
@@ -387,10 +386,39 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
IsSuccessful = false,
|
IsSuccessful = false,
|
||||||
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
|
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
|
||||||
Message = swapInfo.Error ?? swapInfo.Message ?? "Swap execution failed",
|
Message = swapInfo.Error ?? swapInfo.Message ?? "Swap execution failed",
|
||||||
ShouldStopBot = false
|
ShouldStopBot = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException ex) when (ex.InnerException is InsufficientFundsException insufficientFundsEx)
|
||||||
|
{
|
||||||
|
// Handle allowance exception (insufficient ETH for gas fees)
|
||||||
|
_logger.LogError(insufficientFundsEx,
|
||||||
|
"Insufficient funds during autoswap for agent {UserId}: {ErrorMessage}",
|
||||||
|
this.GetPrimaryKeyLong(), insufficientFundsEx.Message);
|
||||||
|
|
||||||
|
return new BalanceCheckResult
|
||||||
|
{
|
||||||
|
IsSuccessful = false,
|
||||||
|
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
|
||||||
|
Message = insufficientFundsEx.UserMessage ??
|
||||||
|
"Insufficient ETH for gas fees during autoswap. Bot cannot continue trading.",
|
||||||
|
ShouldStopBot = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during autoswap for agent {UserId}, bot {RequestingBotId}",
|
||||||
|
this.GetPrimaryKeyLong(), requestingBotId);
|
||||||
|
|
||||||
|
return new BalanceCheckResult
|
||||||
|
{
|
||||||
|
IsSuccessful = false,
|
||||||
|
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
|
||||||
|
Message = ex.Message,
|
||||||
|
ShouldStopBot = true
|
||||||
|
};
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Always clear the swap in progress flag
|
// Always clear the swap in progress flag
|
||||||
@@ -398,24 +426,6 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
await _state.WriteStateAsync();
|
await _state.WriteStateAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error checking/ensuring ETH balance for agent {UserId}, bot {RequestingBotId}",
|
|
||||||
this.GetPrimaryKeyLong(), requestingBotId);
|
|
||||||
|
|
||||||
// Clear swap in progress flag on error
|
|
||||||
_state.State.IsSwapInProgress = false;
|
|
||||||
await _state.WriteStateAsync();
|
|
||||||
|
|
||||||
return new BalanceCheckResult
|
|
||||||
{
|
|
||||||
IsSuccessful = false,
|
|
||||||
FailureReason = BalanceCheckFailureReason.BalanceFetchError,
|
|
||||||
Message = ex.Message,
|
|
||||||
ShouldStopBot = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets cached balance data or fetches fresh data if cache is invalid/expired
|
/// Gets cached balance data or fetches fresh data if cache is invalid/expired
|
||||||
|
|||||||
@@ -1,94 +1,110 @@
|
|||||||
|
#nullable enable
|
||||||
namespace Managing.Application.Bots.Models
|
namespace Managing.Application.Bots.Models
|
||||||
{
|
{
|
||||||
|
[GenerateSerializer]
|
||||||
public class AgentGrainState
|
public class AgentGrainState
|
||||||
{
|
{
|
||||||
public string AgentName { get; set; }
|
[Id(0)] public string AgentName { get; set; } = string.Empty;
|
||||||
public HashSet<Guid> BotIds { get; set; } = new HashSet<Guid>();
|
[Id(1)] public HashSet<Guid> BotIds { get; set; } = new HashSet<Guid>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks if a swap operation is currently in progress to prevent multiple simultaneous swaps
|
/// Tracks if a swap operation is currently in progress to prevent multiple simultaneous swaps
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(2)]
|
||||||
public bool IsSwapInProgress { get; set; } = false;
|
public bool IsSwapInProgress { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timestamp of the last swap operation to implement cooldown period
|
/// Timestamp of the last swap operation to implement cooldown period
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(3)]
|
||||||
public DateTime? LastSwapTime { get; set; } = null;
|
public DateTime? LastSwapTime { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cached balance data to reduce external API calls
|
/// Cached balance data to reduce external API calls
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(4)]
|
||||||
public CachedBalanceData? CachedBalanceData { get; set; } = null;
|
public CachedBalanceData? CachedBalanceData { get; set; } = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cached balance data to avoid repeated external API calls
|
/// Cached balance data to avoid repeated external API calls
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[GenerateSerializer]
|
||||||
public class CachedBalanceData
|
public class CachedBalanceData
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When the balance was last fetched
|
/// When the balance was last fetched
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(0)]
|
||||||
public DateTime LastFetched { get; set; } = DateTime.UtcNow;
|
public DateTime LastFetched { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The account name this balance data is for
|
/// The account name this balance data is for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(1)]
|
||||||
public string AccountName { get; set; } = string.Empty;
|
public string AccountName { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ETH balance in USD
|
/// ETH balance in USD
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(2)]
|
||||||
public decimal EthValueInUsd { get; set; } = 0;
|
public decimal EthValueInUsd { get; set; } = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// USDC balance value
|
/// USDC balance value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(3)]
|
||||||
public decimal UsdcValue { get; set; } = 0;
|
public decimal UsdcValue { get; set; } = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the cached data is still valid (less than 1 minute old)
|
/// Whether the cached data is still valid (less than 1 minute old)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsValid => DateTime.UtcNow - LastFetched < TimeSpan.FromMinutes(1.5);
|
public bool IsValid => DateTime.UtcNow - LastFetched < TimeSpan.FromMinutes(1.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result of a balance check operation
|
/// Result of a balance check operation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BalanceCheckResult
|
[GenerateSerializer]
|
||||||
{
|
public class BalanceCheckResult
|
||||||
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the balance check was successful
|
/// Whether the balance check was successful
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(0)]
|
||||||
public bool IsSuccessful { get; set; }
|
public bool IsSuccessful { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The reason for failure if not successful
|
/// The reason for failure if not successful
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(1)]
|
||||||
public BalanceCheckFailureReason FailureReason { get; set; }
|
public BalanceCheckFailureReason FailureReason { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Additional details about the result
|
/// Additional details about the result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(2)]
|
||||||
public string Message { get; set; } = string.Empty;
|
public string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the bot should stop due to this result
|
/// Whether the bot should stop due to this result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Id(3)]
|
||||||
public bool ShouldStopBot { get; set; }
|
public bool ShouldStopBot { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reasons why a balance check might fail
|
/// Reasons why a balance check might fail
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum BalanceCheckFailureReason
|
public enum BalanceCheckFailureReason
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
InsufficientUsdcBelowMinimum,
|
InsufficientUsdcBelowMinimum,
|
||||||
InsufficientUsdcForSwap,
|
InsufficientUsdcForSwap,
|
||||||
SwapInProgress,
|
SwapInProgress,
|
||||||
SwapCooldownActive,
|
SwapCooldownActive,
|
||||||
BalanceFetchError,
|
BalanceFetchError,
|
||||||
SwapExecutionError
|
SwapExecutionError,
|
||||||
}
|
InsufficientEthBelowMinimum
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ using Managing.Application.Trading.Commands;
|
|||||||
using Managing.Application.Trading.Handlers;
|
using Managing.Application.Trading.Handlers;
|
||||||
using Managing.Common;
|
using Managing.Common;
|
||||||
using Managing.Core;
|
using Managing.Core;
|
||||||
|
using Managing.Core.Exceptions;
|
||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
using Managing.Domain.Candles;
|
using Managing.Domain.Candles;
|
||||||
@@ -822,6 +823,17 @@ public class TradingBotBase : ITradingBot
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
catch (InsufficientFundsException ex)
|
||||||
|
{
|
||||||
|
// Handle insufficient funds errors with user-friendly messaging
|
||||||
|
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||||
|
await LogWarning(ex.UserMessage);
|
||||||
|
|
||||||
|
// Log the technical details for debugging
|
||||||
|
Logger.LogError(ex, "Insufficient funds error for signal {SignalId}: {ErrorMessage}", signal.Identifier, ex.Message);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ using Managing.Application.Abstractions.Grains;
|
|||||||
using Managing.Application.Abstractions.Repositories;
|
using Managing.Application.Abstractions.Repositories;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.Bots;
|
using Managing.Application.Bots;
|
||||||
|
using Managing.Application.Bots.Models;
|
||||||
|
using Managing.Common;
|
||||||
using Managing.Core;
|
using Managing.Core;
|
||||||
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
using Managing.Domain.Scenarios;
|
using Managing.Domain.Scenarios;
|
||||||
using Managing.Domain.Shared.Helpers;
|
using Managing.Domain.Shared.Helpers;
|
||||||
@@ -103,12 +106,31 @@ namespace Managing.Application.ManageBot
|
|||||||
}
|
}
|
||||||
|
|
||||||
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(identifier);
|
||||||
|
|
||||||
|
// Check balances for EVM/GMX V2 bots before starting/restarting
|
||||||
|
var botConfig = await botGrain.GetConfiguration();
|
||||||
|
var account = await ServiceScopeHelpers.WithScopedService<IAccountService, Account>(
|
||||||
|
_scopeFactory,
|
||||||
|
async accountService => await accountService.GetAccount(botConfig.AccountName, true, false));
|
||||||
|
|
||||||
|
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
|
||||||
|
{
|
||||||
|
var balanceCheckResult = await CheckAccountBalancesAsync(account);
|
||||||
|
if (!balanceCheckResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
_tradingBotLogger.LogWarning(
|
||||||
|
"Bot {Identifier} restart blocked due to insufficient balances: {Message}",
|
||||||
|
identifier, balanceCheckResult.Message);
|
||||||
|
throw new InvalidOperationException(balanceCheckResult.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var grainState = await botGrain.GetBotDataAsync();
|
||||||
|
|
||||||
if (previousStatus == BotStatus.Saved)
|
if (previousStatus == BotStatus.Saved)
|
||||||
{
|
{
|
||||||
// First time startup
|
// First time startup
|
||||||
await botGrain.StartAsync();
|
await botGrain.StartAsync();
|
||||||
var grainState = await botGrain.GetBotDataAsync();
|
|
||||||
var account = await botGrain.GetAccount();
|
|
||||||
var startupMessage = $"🚀 **Bot Started**\n\n" +
|
var startupMessage = $"🚀 **Bot Started**\n\n" +
|
||||||
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
||||||
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
|
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
|
||||||
@@ -122,8 +144,6 @@ namespace Managing.Application.ManageBot
|
|||||||
{
|
{
|
||||||
// Restart (bot was previously down)
|
// Restart (bot was previously down)
|
||||||
await botGrain.RestartAsync();
|
await botGrain.RestartAsync();
|
||||||
var grainState = await botGrain.GetBotDataAsync();
|
|
||||||
var account = await botGrain.GetAccount();
|
|
||||||
var restartMessage = $"🔄 **Bot Restarted**\n\n" +
|
var restartMessage = $"🔄 **Bot Restarted**\n\n" +
|
||||||
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
$"🎯 **Agent:** {account.User.AgentName}\n" +
|
||||||
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
|
$"🤖 **Bot Name:** {grainState.Config.Name}\n" +
|
||||||
@@ -138,8 +158,8 @@ namespace Managing.Application.ManageBot
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_tradingBotLogger.LogError(e, "Error restarting bot {Identifier}", identifier);
|
SentrySdk.CaptureException(e);
|
||||||
return BotStatus.Stopped;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +317,6 @@ namespace Managing.Application.ManageBot
|
|||||||
var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier);
|
var existingBot = await _botRepository.GetBotByIdentifierAsync(bot.Identifier);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check if bot already exists in database
|
// Check if bot already exists in database
|
||||||
await ServiceScopeHelpers.WithScopedService<IBotRepository>(
|
await ServiceScopeHelpers.WithScopedService<IBotRepository>(
|
||||||
_scopeFactory,
|
_scopeFactory,
|
||||||
@@ -365,5 +384,79 @@ namespace Managing.Application.ManageBot
|
|||||||
sortDirection);
|
sortDirection);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks USDC and ETH balances for EVM/GMX V2 accounts
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="account">The account to check balances for</param>
|
||||||
|
/// <returns>Balance check result</returns>
|
||||||
|
public async Task<BalanceCheckResult> CheckAccountBalancesAsync(Account account)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await ServiceScopeHelpers
|
||||||
|
.WithScopedServices<IExchangeService, IAccountService, BalanceCheckResult>(
|
||||||
|
_scopeFactory,
|
||||||
|
async (exchangeService, accountService) =>
|
||||||
|
{
|
||||||
|
// Get current balances
|
||||||
|
var balances = await exchangeService.GetBalances(account);
|
||||||
|
var ethBalance = balances.FirstOrDefault(b => b.TokenName?.ToUpper() == "ETH");
|
||||||
|
var usdcBalance = balances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC");
|
||||||
|
|
||||||
|
var ethValueInUsd = ethBalance?.Amount * ethBalance?.Price ?? 0;
|
||||||
|
var usdcValue = usdcBalance?.Value ?? 0;
|
||||||
|
|
||||||
|
_tradingBotLogger.LogInformation(
|
||||||
|
"Balance check for bot restart - Account: {AccountName}, ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD",
|
||||||
|
account.Name, ethValueInUsd, usdcValue);
|
||||||
|
|
||||||
|
// Check USDC minimum balance
|
||||||
|
if (usdcValue < Constants.GMX.Config.MinimumPositionAmount)
|
||||||
|
{
|
||||||
|
return new BalanceCheckResult
|
||||||
|
{
|
||||||
|
IsSuccessful = false,
|
||||||
|
FailureReason = BalanceCheckFailureReason.InsufficientUsdcBelowMinimum,
|
||||||
|
Message =
|
||||||
|
$"USDC balance ({usdcValue:F2} USD) is below minimum required amount ({Constants.GMX.Config.MinimumPositionAmount} USD). Please add more USDC to restart the bot.",
|
||||||
|
ShouldStopBot = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ETH minimum balance for trading
|
||||||
|
if (ethValueInUsd < Constants.GMX.Config.MinimumTradeEthBalanceUsd)
|
||||||
|
{
|
||||||
|
return new BalanceCheckResult
|
||||||
|
{
|
||||||
|
IsSuccessful = false,
|
||||||
|
FailureReason = BalanceCheckFailureReason.InsufficientEthBelowMinimum,
|
||||||
|
Message =
|
||||||
|
$"ETH balance ({ethValueInUsd:F2} USD) is below minimum required amount ({Constants.GMX.Config.MinimumTradeEthBalanceUsd} USD) for trading. Please add more ETH to restart the bot.",
|
||||||
|
ShouldStopBot = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BalanceCheckResult
|
||||||
|
{
|
||||||
|
IsSuccessful = true,
|
||||||
|
FailureReason = BalanceCheckFailureReason.None,
|
||||||
|
Message = "Balance check successful - Sufficient USDC and ETH balances",
|
||||||
|
ShouldStopBot = false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_tradingBotLogger.LogError(ex, "Error checking balances for account {AccountName}", account.Name);
|
||||||
|
return new BalanceCheckResult
|
||||||
|
{
|
||||||
|
IsSuccessful = false,
|
||||||
|
FailureReason = BalanceCheckFailureReason.BalanceFetchError,
|
||||||
|
Message = $"Failed to check balances: {ex.Message}",
|
||||||
|
ShouldStopBot = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Managing.Application.Abstractions.Grains;
|
using Managing.Application.Abstractions;
|
||||||
|
using Managing.Application.Abstractions.Grains;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
using Managing.Common;
|
using Managing.Common;
|
||||||
@@ -11,12 +12,14 @@ namespace Managing.Application.ManageBot
|
|||||||
{
|
{
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IGrainFactory _grainFactory;
|
private readonly IGrainFactory _grainFactory;
|
||||||
|
private readonly IBotService _botService;
|
||||||
|
|
||||||
public StartBotCommandHandler(
|
public StartBotCommandHandler(
|
||||||
IAccountService accountService, IGrainFactory grainFactory)
|
IAccountService accountService, IGrainFactory grainFactory, IBotService botService)
|
||||||
{
|
{
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_grainFactory = grainFactory;
|
_grainFactory = grainFactory;
|
||||||
|
_botService = botService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BotStatus> Handle(StartBotCommand request, CancellationToken cancellationToken)
|
public async Task<BotStatus> Handle(StartBotCommand request, CancellationToken cancellationToken)
|
||||||
@@ -39,13 +42,26 @@ namespace Managing.Application.ManageBot
|
|||||||
$"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
|
$"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var account = await _accountService.GetAccount(request.Config.AccountName, true, true);
|
var account = await _accountService.GetAccount(request.Config.AccountName, true, false);
|
||||||
|
|
||||||
if (account == null)
|
if (account == null)
|
||||||
{
|
{
|
||||||
throw new Exception($"Account {request.Config.AccountName} not found");
|
throw new Exception($"Account {request.Config.AccountName} not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check balances for EVM/GMX V2 accounts before starting
|
||||||
|
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
|
||||||
|
{
|
||||||
|
var balanceCheckResult = await _botService.CheckAccountBalancesAsync(account);
|
||||||
|
if (!balanceCheckResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(balanceCheckResult.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other exchanges, keep the original USDC balance check
|
||||||
|
if (account.Exchange != TradingExchanges.Evm && account.Exchange != TradingExchanges.GmxV2)
|
||||||
|
{
|
||||||
var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
|
var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
|
||||||
|
|
||||||
if (usdcBalance == null ||
|
if (usdcBalance == null ||
|
||||||
@@ -54,6 +70,7 @@ namespace Managing.Application.ManageBot
|
|||||||
{
|
{
|
||||||
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
|
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.Trading.Commands;
|
using Managing.Application.Trading.Commands;
|
||||||
using Managing.Common;
|
using Managing.Common;
|
||||||
|
using Managing.Core.Exceptions;
|
||||||
using Managing.Domain.Shared.Helpers;
|
using Managing.Domain.Shared.Helpers;
|
||||||
using Managing.Domain.Trades;
|
using Managing.Domain.Trades;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
@@ -19,7 +20,6 @@ namespace Managing.Application.Trading.Handlers
|
|||||||
{
|
{
|
||||||
var account = await accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false);
|
var account = await accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false);
|
||||||
|
|
||||||
|
|
||||||
var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator;
|
var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator;
|
||||||
var position = new Position(Guid.NewGuid(), request.AccountName, request.Direction,
|
var position = new Position(Guid.NewGuid(), request.AccountName, request.Direction,
|
||||||
request.Ticker,
|
request.Ticker,
|
||||||
@@ -44,6 +44,18 @@ namespace Managing.Application.Trading.Handlers
|
|||||||
$"Bot trading balance of {balanceToRisk} USD is less than the minimum {Constants.GMX.Config.MinimumPositionAmount} USD required to trade");
|
$"Bot trading balance of {balanceToRisk} USD is less than the minimum {Constants.GMX.Config.MinimumPositionAmount} USD required to trade");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gas fee check for EVM exchanges
|
||||||
|
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
|
||||||
|
{
|
||||||
|
var gasFeeUsd = await exchangeService.GetFee(account);
|
||||||
|
if (gasFeeUsd > Constants.GMX.Config.MaximumGasFeeUsd)
|
||||||
|
{
|
||||||
|
throw new InsufficientFundsException(
|
||||||
|
$"Gas fee too high for position opening: {gasFeeUsd:F2} USD (threshold: {Constants.GMX.Config.MaximumGasFeeUsd} USD). Position opening cancelled.",
|
||||||
|
InsufficientFundsType.InsufficientEth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var price = request.IsForPaperTrading && request.Price.HasValue
|
var price = request.IsForPaperTrading && request.Price.HasValue
|
||||||
? request.Price.Value
|
? request.Price.Value
|
||||||
: await exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
|
: await exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ namespace Managing.Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
public const decimal MinimumPositionAmount = 5m;
|
public const decimal MinimumPositionAmount = 5m;
|
||||||
public const decimal MinimumEthBalance = 2m;
|
public const decimal MinimumTradeEthBalanceUsd = 1.5m;
|
||||||
|
public static decimal MinimumSwapEthBalanceUsd = 1m;
|
||||||
|
public const decimal MaximumGasFeeUsd = 1.5m;
|
||||||
public const double AutoSwapAmount = 3;
|
public const double AutoSwapAmount = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,3 +76,78 @@ public class ServiceUnavailableException : Exception
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when there are insufficient funds or allowance for a transaction
|
||||||
|
/// This typically indicates the user needs to add more ETH for gas or approve token spending
|
||||||
|
/// </summary>
|
||||||
|
public class InsufficientFundsException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type of insufficient funds error
|
||||||
|
/// </summary>
|
||||||
|
public InsufficientFundsType ErrorType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User-friendly message explaining what needs to be done
|
||||||
|
/// </summary>
|
||||||
|
public string UserMessage { get; }
|
||||||
|
|
||||||
|
public InsufficientFundsException(string errorMessage, InsufficientFundsType errorType)
|
||||||
|
: base(errorMessage)
|
||||||
|
{
|
||||||
|
ErrorType = errorType;
|
||||||
|
UserMessage = GetUserFriendlyMessage(errorType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetUserFriendlyMessage(InsufficientFundsType errorType)
|
||||||
|
{
|
||||||
|
return errorType switch
|
||||||
|
{
|
||||||
|
InsufficientFundsType.InsufficientEth =>
|
||||||
|
"❌ **Insufficient ETH for Gas Fees**\n" +
|
||||||
|
"Your wallet doesn't have enough ETH to pay for transaction gas fees.\n" +
|
||||||
|
"Please add ETH to your wallet and try again.",
|
||||||
|
|
||||||
|
InsufficientFundsType.InsufficientAllowance =>
|
||||||
|
"❌ **Insufficient Token Allowance**\n" +
|
||||||
|
"The trading contract doesn't have permission to spend your tokens.\n" +
|
||||||
|
"Please approve token spending in your wallet and try again.",
|
||||||
|
|
||||||
|
InsufficientFundsType.InsufficientBalance =>
|
||||||
|
"❌ **Insufficient Token Balance**\n" +
|
||||||
|
"Your wallet doesn't have enough tokens for this trade.\n" +
|
||||||
|
"Please add more tokens to your wallet and try again.",
|
||||||
|
|
||||||
|
_ => "❌ **Transaction Failed**\n" +
|
||||||
|
"The transaction failed due to insufficient funds.\n" +
|
||||||
|
"Please check your wallet balance and try again."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Types of insufficient funds errors
|
||||||
|
/// </summary>
|
||||||
|
public enum InsufficientFundsType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Not enough ETH for gas fees
|
||||||
|
/// </summary>
|
||||||
|
InsufficientEth,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Token allowance is insufficient (ERC20: transfer amount exceeds allowance)
|
||||||
|
/// </summary>
|
||||||
|
InsufficientAllowance,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Token balance is insufficient
|
||||||
|
/// </summary>
|
||||||
|
InsufficientBalance,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// General insufficient funds error
|
||||||
|
/// </summary>
|
||||||
|
General
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ public interface IExchangeProcessor
|
|||||||
decimal GetVolume(Account account, Ticker ticker);
|
decimal GetVolume(Account account, Ticker ticker);
|
||||||
Task<List<Trade>> GetTrades(Account account, Ticker ticker);
|
Task<List<Trade>> GetTrades(Account account, Ticker ticker);
|
||||||
Task<bool> CancelOrder(Account account, Ticker ticker);
|
Task<bool> CancelOrder(Account account, Ticker ticker);
|
||||||
decimal GetFee(Account account, bool isForPaperTrading = false);
|
Task<decimal> GetFee(Account account, bool isForPaperTrading = false);
|
||||||
Task<Candle> GetCandle(Account account, Ticker ticker, DateTime date);
|
Task<Candle> GetCandle(Account account, Ticker ticker, DateTime date);
|
||||||
Task<decimal> GetQuantityInPosition(Account account, Ticker ticker);
|
Task<decimal> GetQuantityInPosition(Account account, Ticker ticker);
|
||||||
Orderbook GetOrderbook(Account account, Ticker ticker);
|
Orderbook GetOrderbook(Account account, Ticker ticker);
|
||||||
|
|||||||
@@ -51,6 +51,21 @@ namespace Managing.Infrastructure.Exchanges
|
|||||||
reduceOnly ? TradeStatus.PendingOpen : TradeStatus.Filled);
|
reduceOnly ? TradeStatus.PendingOpen : TradeStatus.Filled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check gas fees for EVM exchanges before opening position
|
||||||
|
if (IsEvmExchange(account))
|
||||||
|
{
|
||||||
|
var gasFeeUsd = await GetFee(account);
|
||||||
|
if (gasFeeUsd > 0.5m)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
$"Gas fee too high for position opening: {gasFeeUsd:F2} USD (threshold: 0.5 USD). Cancelling position opening.");
|
||||||
|
|
||||||
|
// Return a cancelled trade
|
||||||
|
return BuildEmptyTrade(ticker, price, quantity, direction, leverage, tradeType, currentDate.Value,
|
||||||
|
TradeStatus.Cancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var processor = GetProcessor(account);
|
var processor = GetProcessor(account);
|
||||||
return await processor.OpenTrade(account, ticker, direction, price, quantity, leverage, tradeType,
|
return await processor.OpenTrade(account, ticker, direction, price, quantity, leverage, tradeType,
|
||||||
reduceOnly, isForPaperTrading, currentDate, ioc, stopLossPrice, takeProfitPrice);
|
reduceOnly, isForPaperTrading, currentDate, ioc, stopLossPrice, takeProfitPrice);
|
||||||
@@ -242,10 +257,10 @@ namespace Managing.Infrastructure.Exchanges
|
|||||||
return await processor.GetBalance(account);
|
return await processor.GetBalance(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
public decimal GetFee(Account account, bool isForPaperTrading = false)
|
public async Task<decimal> GetFee(Account account, bool isForPaperTrading = false)
|
||||||
{
|
{
|
||||||
var processor = GetProcessor(account);
|
var processor = GetProcessor(account);
|
||||||
return processor.GetFee(account);
|
return await processor.GetFee(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<decimal> GetPrice(Account account, Ticker ticker, DateTime date)
|
public async Task<decimal> GetPrice(Account account, Ticker ticker, DateTime date)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace Managing.Infrastructure.Exchanges.Exchanges
|
|||||||
public abstract Task<decimal> GetBalance(Account account, bool isForPaperTrading = false);
|
public abstract Task<decimal> GetBalance(Account account, bool isForPaperTrading = false);
|
||||||
public abstract Task<Candle> GetCandle(Account account, Ticker ticker, DateTime date);
|
public abstract Task<Candle> GetCandle(Account account, Ticker ticker, DateTime date);
|
||||||
public abstract Task<List<Candle>> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval);
|
public abstract Task<List<Candle>> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval);
|
||||||
public abstract decimal GetFee(Account account, bool isForPaperTrading = false);
|
public abstract Task<decimal> GetFee(Account account, bool isForPaperTrading = false);
|
||||||
public abstract Task<decimal> GetPrice(Account account, Ticker ticker, DateTime date);
|
public abstract Task<decimal> GetPrice(Account account, Ticker ticker, DateTime date);
|
||||||
public abstract Task<decimal> GetCurrentPrice(Account account, Ticker ticker);
|
public abstract Task<decimal> GetCurrentPrice(Account account, Ticker ticker);
|
||||||
public abstract Task<decimal> GetQuantityInPosition(Account account, Ticker ticker);
|
public abstract Task<decimal> GetQuantityInPosition(Account account, Ticker ticker);
|
||||||
|
|||||||
@@ -78,9 +78,9 @@ public class EvmProcessor : BaseProcessor
|
|||||||
return await _evmManager.GetCandles(ticker, startDate, interval, isFirstCall);
|
return await _evmManager.GetCandles(ticker, startDate, interval, isFirstCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override decimal GetFee(Account account, bool isForPaperTrading = false)
|
public override async Task<decimal> GetFee(Account account, bool isForPaperTrading = false)
|
||||||
{
|
{
|
||||||
return _evmManager.GetFee(Constants.Chains.Arbitrum).Result;
|
return await _evmManager.GetFee(Constants.Chains.Arbitrum);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<decimal> GetPrice(Account account, Ticker ticker, DateTime date)
|
public override async Task<decimal> GetPrice(Account account, Ticker ticker, DateTime date)
|
||||||
|
|||||||
@@ -137,10 +137,10 @@ namespace Managing.Infrastructure.Tests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(TradingExchanges.Evm)]
|
[InlineData(TradingExchanges.Evm)]
|
||||||
public void Should_Return_Fee(TradingExchanges exchange)
|
public async Task Should_Return_Fee(TradingExchanges exchange)
|
||||||
{
|
{
|
||||||
var account = PrivateKeys.GetAccount();
|
var account = PrivateKeys.GetAccount();
|
||||||
var fee = _exchangeService.GetFee(account);
|
var fee = await _exchangeService.GetFee(account);
|
||||||
Assert.IsType<decimal>(fee);
|
Assert.IsType<decimal>(fee);
|
||||||
Assert.True(fee > 0);
|
Assert.True(fee > 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -891,11 +891,15 @@ public class EvmManager : IEvmManager
|
|||||||
|
|
||||||
public async Task<decimal> GetFee(string chainName)
|
public async Task<decimal> GetFee(string chainName)
|
||||||
{
|
{
|
||||||
var chain = ChainService.GetChain(chainName);
|
try
|
||||||
var web3 = new Web3(chain.RpcUrl);
|
{
|
||||||
var etherPrice = (await GetPrices(new List<string> { "ethereum" }))["ethereum"]["usd"];
|
return await _web3ProxyService.GetEstimatedGasFeeUsdAsync();
|
||||||
var fee = await GmxService.GetFee(web3, etherPrice);
|
}
|
||||||
return fee;
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error getting estimated gas fee: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Trade>> GetOrders(Account account, Ticker ticker)
|
public async Task<List<Trade>> GetOrders(Account account, Ticker ticker)
|
||||||
|
|||||||
@@ -151,4 +151,28 @@ namespace Managing.Infrastructure.Evm.Models.Proxy
|
|||||||
[JsonPropertyName("balances")]
|
[JsonPropertyName("balances")]
|
||||||
public List<Balance> Balances { get; set; }
|
public List<Balance> Balances { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for gas fee information
|
||||||
|
/// </summary>
|
||||||
|
public class GasFeeResponse : Web3ProxyResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Estimated gas fee in USD
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("estimatedGasFeeUsd")]
|
||||||
|
public double? EstimatedGasFeeUsd { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current ETH price in USD
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("ethPrice")]
|
||||||
|
public double? EthPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gas price in Gwei
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("gasPriceGwei")]
|
||||||
|
public double? GasPriceGwei { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Core.Exceptions;
|
||||||
using Managing.Domain.Accounts;
|
using Managing.Domain.Accounts;
|
||||||
using Managing.Infrastructure.Evm.Models.Proxy;
|
using Managing.Infrastructure.Evm.Models.Proxy;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -77,6 +78,64 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
statusCode == HttpStatusCode.GatewayTimeout;
|
statusCode == HttpStatusCode.GatewayTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an error message indicates insufficient funds or allowance that should not be retried
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errorMessage">The error message to check</param>
|
||||||
|
/// <returns>True if this is a non-retryable insufficient funds error</returns>
|
||||||
|
private static bool IsInsufficientFundsError(string errorMessage)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(errorMessage))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var lowerError = errorMessage.ToLowerInvariant();
|
||||||
|
|
||||||
|
// Check for common insufficient funds/allowance error patterns
|
||||||
|
return lowerError.Contains("erc20: transfer amount exceeds allowance") ||
|
||||||
|
lowerError.Contains("insufficient funds") ||
|
||||||
|
lowerError.Contains("insufficient balance") ||
|
||||||
|
lowerError.Contains("execution reverted") &&
|
||||||
|
(lowerError.Contains("allowance") || lowerError.Contains("balance")) ||
|
||||||
|
lowerError.Contains("gas required exceeds allowance") ||
|
||||||
|
lowerError.Contains("out of gas") ||
|
||||||
|
lowerError.Contains("insufficient eth") ||
|
||||||
|
lowerError.Contains("insufficient token");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the type of insufficient funds error based on the error message
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errorMessage">The error message to analyze</param>
|
||||||
|
/// <returns>The type of insufficient funds error</returns>
|
||||||
|
private static InsufficientFundsType GetInsufficientFundsType(string errorMessage)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(errorMessage))
|
||||||
|
return InsufficientFundsType.General;
|
||||||
|
|
||||||
|
var lowerError = errorMessage.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (lowerError.Contains("erc20: transfer amount exceeds allowance") ||
|
||||||
|
lowerError.Contains("allowance"))
|
||||||
|
{
|
||||||
|
return InsufficientFundsType.InsufficientAllowance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowerError.Contains("gas required exceeds allowance") ||
|
||||||
|
lowerError.Contains("out of gas") ||
|
||||||
|
lowerError.Contains("insufficient eth"))
|
||||||
|
{
|
||||||
|
return InsufficientFundsType.InsufficientEth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowerError.Contains("insufficient balance") ||
|
||||||
|
lowerError.Contains("insufficient token"))
|
||||||
|
{
|
||||||
|
return InsufficientFundsType.InsufficientBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InsufficientFundsType.General;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName)
|
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<HttpResponseMessage>> httpCall, string operationName)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -91,6 +150,11 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
||||||
return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
|
return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
|
||||||
}
|
}
|
||||||
|
catch (InsufficientFundsException)
|
||||||
|
{
|
||||||
|
// Re-throw insufficient funds exceptions immediately without retrying
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex) when (!(ex is Web3ProxyException))
|
catch (Exception ex) when (!(ex is Web3ProxyException))
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Operation {OperationName} failed after all retry attempts", operationName);
|
_logger.LogError(ex, "Operation {OperationName} failed after all retry attempts", operationName);
|
||||||
@@ -113,6 +177,11 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
|
||||||
return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
|
return result ?? throw new Web3ProxyException($"Failed to deserialize response for {operationName}");
|
||||||
}
|
}
|
||||||
|
catch (InsufficientFundsException)
|
||||||
|
{
|
||||||
|
// Re-throw insufficient funds exceptions immediately without retrying
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex) when (!(ex is Web3ProxyException))
|
catch (Exception ex) when (!(ex is Web3ProxyException))
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Operation {OperationName} failed after all retry attempts (IdempotencyKey: {IdempotencyKey})", operationName, idempotencyKey);
|
_logger.LogError(ex, "Operation {OperationName} failed after all retry attempts (IdempotencyKey: {IdempotencyKey})", operationName, idempotencyKey);
|
||||||
@@ -325,6 +394,23 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
return response.Balances ?? new List<Balance>();
|
return response.Balances ?? new List<Balance>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<decimal> GetEstimatedGasFeeUsdAsync()
|
||||||
|
{
|
||||||
|
var response = await GetGmxServiceAsync<GasFeeResponse>("/gas-fee", null);
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
{
|
||||||
|
throw new Web3ProxyException("Gas fee response is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.Success)
|
||||||
|
{
|
||||||
|
throw new Web3ProxyException($"Gas fee request failed: {response.Error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (decimal)(response.EstimatedGasFeeUsd ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task HandleErrorResponse(HttpResponseMessage response)
|
private async Task HandleErrorResponse(HttpResponseMessage response)
|
||||||
{
|
{
|
||||||
var statusCode = (int)response.StatusCode;
|
var statusCode = (int)response.StatusCode;
|
||||||
@@ -337,6 +423,13 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
|
|
||||||
if (errorResponse != null && !errorResponse.Success && !string.IsNullOrEmpty(errorResponse.Error))
|
if (errorResponse != null && !errorResponse.Success && !string.IsNullOrEmpty(errorResponse.Error))
|
||||||
{
|
{
|
||||||
|
// Check if this is an insufficient funds error that should not be retried
|
||||||
|
if (IsInsufficientFundsError(errorResponse.Error))
|
||||||
|
{
|
||||||
|
var errorType = GetInsufficientFundsType(errorResponse.Error);
|
||||||
|
throw new InsufficientFundsException(errorResponse.Error, errorType);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle the standard Web3Proxy error format
|
// Handle the standard Web3Proxy error format
|
||||||
throw new Web3ProxyException(errorResponse.Error);
|
throw new Web3ProxyException(errorResponse.Error);
|
||||||
}
|
}
|
||||||
@@ -351,20 +444,53 @@ namespace Managing.Infrastructure.Evm.Services
|
|||||||
if (structuredErrorResponse?.ErrorDetails != null)
|
if (structuredErrorResponse?.ErrorDetails != null)
|
||||||
{
|
{
|
||||||
structuredErrorResponse.ErrorDetails.StatusCode = statusCode;
|
structuredErrorResponse.ErrorDetails.StatusCode = statusCode;
|
||||||
|
|
||||||
|
// Check if this is an insufficient funds error that should not be retried
|
||||||
|
if (IsInsufficientFundsError(structuredErrorResponse.ErrorDetails.Message))
|
||||||
|
{
|
||||||
|
var errorType = GetInsufficientFundsType(structuredErrorResponse.ErrorDetails.Message);
|
||||||
|
throw new InsufficientFundsException(structuredErrorResponse.ErrorDetails.Message, errorType);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Web3ProxyException(structuredErrorResponse.ErrorDetails);
|
throw new Web3ProxyException(structuredErrorResponse.ErrorDetails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) when (ex is InsufficientFundsException)
|
||||||
|
{
|
||||||
|
// Re-throw insufficient funds exceptions as-is
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
// Check if the raw content contains insufficient funds errors
|
||||||
|
if (IsInsufficientFundsError(content))
|
||||||
|
{
|
||||||
|
var errorType = GetInsufficientFundsType(content);
|
||||||
|
throw new InsufficientFundsException(content, errorType);
|
||||||
|
}
|
||||||
|
|
||||||
// If we couldn't parse as structured error, use the simple error or fallback
|
// If we couldn't parse as structured error, use the simple error or fallback
|
||||||
throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
|
throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) when (ex is InsufficientFundsException)
|
||||||
|
{
|
||||||
|
// Re-throw insufficient funds exceptions as-is
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex) when (!(ex is Web3ProxyException))
|
catch (Exception ex) when (!(ex is Web3ProxyException))
|
||||||
{
|
{
|
||||||
SentrySdk.CaptureException(ex);
|
SentrySdk.CaptureException(ex);
|
||||||
// If we couldn't parse the error as JSON or another issue occurred
|
// If we couldn't parse the error as JSON or another issue occurred
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Check if the raw content contains insufficient funds errors
|
||||||
|
if (IsInsufficientFundsError(content))
|
||||||
|
{
|
||||||
|
var errorType = GetInsufficientFundsType(content);
|
||||||
|
throw new InsufficientFundsException(content, errorType);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
|
throw new Web3ProxyException($"HTTP error {statusCode}: {content}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {decodeReferralCode, encodeReferralCode} from '../../generated/gmxsdk/uti
|
|||||||
import {formatUsd} from '../../generated/gmxsdk/utils/numbers/formatting.js';
|
import {formatUsd} from '../../generated/gmxsdk/utils/numbers/formatting.js';
|
||||||
import {calculateDisplayDecimals} from '../../generated/gmxsdk/utils/numbers/index.js';
|
import {calculateDisplayDecimals} from '../../generated/gmxsdk/utils/numbers/index.js';
|
||||||
import {handleError} from '../../utils/errorHandler.js';
|
import {handleError} from '../../utils/errorHandler.js';
|
||||||
import {Abi, zeroHash} from 'viem';
|
import {Abi, formatEther, parseEther, zeroHash} from 'viem';
|
||||||
import {CLAIMABLE_FUNDING_AMOUNT} from '../../generated/gmxsdk/configs/dataStore.js';
|
import {CLAIMABLE_FUNDING_AMOUNT} from '../../generated/gmxsdk/configs/dataStore.js';
|
||||||
import {hashDataMap, hashString} from '../../generated/gmxsdk/utils/hash.js';
|
import {hashDataMap, hashString} from '../../generated/gmxsdk/utils/hash.js';
|
||||||
import {ContractName, getContract} from '../../generated/gmxsdk/configs/contracts.js';
|
import {ContractName, getContract} from '../../generated/gmxsdk/configs/contracts.js';
|
||||||
@@ -56,6 +56,7 @@ const MAX_CACHE_SIZE = 5; // Limit cache size to prevent memory issues
|
|||||||
const OPERATION_TIMEOUT = 30000; // 30 seconds timeout for operations
|
const OPERATION_TIMEOUT = 30000; // 30 seconds timeout for operations
|
||||||
|
|
||||||
const MEMORY_WARNING_THRESHOLD = 0.8; // Warn when memory usage exceeds 80%
|
const MEMORY_WARNING_THRESHOLD = 0.8; // Warn when memory usage exceeds 80%
|
||||||
|
const MAX_GAS_FEE_USD = 1; // Maximum gas fee in USD (1 USDC)
|
||||||
|
|
||||||
// Memory monitoring function
|
// Memory monitoring function
|
||||||
function checkMemoryUsage() {
|
function checkMemoryUsage() {
|
||||||
@@ -75,6 +76,118 @@ function checkMemoryUsage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user has sufficient ETH balance for gas fees
|
||||||
|
* @param sdk The GMX SDK client
|
||||||
|
* @param estimatedGasFee The estimated gas fee in wei
|
||||||
|
* @returns Object with balance check result and details
|
||||||
|
*/
|
||||||
|
async function checkGasFeeBalance(
|
||||||
|
sdk: GmxSdk,
|
||||||
|
estimatedGasFee: bigint
|
||||||
|
): Promise<{
|
||||||
|
hasSufficientBalance: boolean;
|
||||||
|
ethBalance: string;
|
||||||
|
estimatedGasFeeUsd: number;
|
||||||
|
ethPrice: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Get ETH balance using the public client
|
||||||
|
const ethBalance = await sdk.publicClient.getBalance({ address: sdk.account });
|
||||||
|
const ethBalanceFormatted = formatEther(ethBalance);
|
||||||
|
|
||||||
|
// Get ETH price from the market data
|
||||||
|
const {tokensData} = await getMarketsInfoWithCache(sdk);
|
||||||
|
const ethTokenData = getTokenDataFromTicker("ETH", tokensData);
|
||||||
|
|
||||||
|
if (!ethTokenData || !ethTokenData.prices?.minPrice) {
|
||||||
|
throw new Error("Unable to get ETH price for gas fee calculation");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert ETH price from 30 decimals to regular number
|
||||||
|
const ethPrice = Number(ethTokenData.prices.minPrice) / 1e30;
|
||||||
|
|
||||||
|
// Calculate estimated gas fee in USD
|
||||||
|
const estimatedGasFeeEth = Number(formatEther(estimatedGasFee));
|
||||||
|
const estimatedGasFeeUsd = estimatedGasFeeEth * ethPrice;
|
||||||
|
|
||||||
|
// Check if gas fee exceeds maximum allowed (1 USDC)
|
||||||
|
const hasSufficientBalance = estimatedGasFeeUsd <= MAX_GAS_FEE_USD;
|
||||||
|
|
||||||
|
console.log(`⛽ Gas fee check:`, {
|
||||||
|
ethBalance: ethBalanceFormatted,
|
||||||
|
estimatedGasFeeEth: estimatedGasFeeEth.toFixed(6),
|
||||||
|
estimatedGasFeeUsd: estimatedGasFeeUsd.toFixed(2),
|
||||||
|
ethPrice: ethPrice.toFixed(2),
|
||||||
|
maxAllowedUsd: MAX_GAS_FEE_USD,
|
||||||
|
hasSufficientBalance
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasSufficientBalance,
|
||||||
|
ethBalance: ethBalanceFormatted,
|
||||||
|
estimatedGasFeeUsd,
|
||||||
|
ethPrice,
|
||||||
|
errorMessage: hasSufficientBalance ? undefined :
|
||||||
|
`Gas fee too high: $${estimatedGasFeeUsd.toFixed(2)} exceeds maximum of $${MAX_GAS_FEE_USD}. Please wait for lower gas fees or add more ETH.`
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking gas fee balance:', error);
|
||||||
|
return {
|
||||||
|
hasSufficientBalance: false,
|
||||||
|
ethBalance: "0",
|
||||||
|
estimatedGasFeeUsd: 0,
|
||||||
|
ethPrice: 0,
|
||||||
|
errorMessage: `Failed to check gas fee balance: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimates gas fee for a position opening transaction
|
||||||
|
* @param sdk The GMX SDK client
|
||||||
|
* @param params The position increase parameters
|
||||||
|
* @returns Estimated gas fee in wei
|
||||||
|
*/
|
||||||
|
async function estimatePositionGasFee(
|
||||||
|
sdk: GmxSdk,
|
||||||
|
params: PositionIncreaseParams
|
||||||
|
): Promise<bigint> {
|
||||||
|
try {
|
||||||
|
// Estimate gas for the position opening transaction
|
||||||
|
// This is a simplified estimation - in practice, you might want to use
|
||||||
|
// the actual transaction simulation or a more sophisticated gas estimation
|
||||||
|
const baseGasLimit = 500000n; // Base gas limit for position opening
|
||||||
|
|
||||||
|
// Get gas price using the public client
|
||||||
|
const feeData = await sdk.publicClient.estimateFeesPerGas({
|
||||||
|
type: "legacy",
|
||||||
|
chain: sdk.chain,
|
||||||
|
});
|
||||||
|
const gasPrice = feeData.gasPrice || 0n;
|
||||||
|
|
||||||
|
// Add some buffer for safety (20% more)
|
||||||
|
const estimatedGas = (baseGasLimit * 120n) / 100n;
|
||||||
|
const estimatedGasFee = estimatedGas * gasPrice;
|
||||||
|
|
||||||
|
console.log(`⛽ Gas estimation:`, {
|
||||||
|
baseGasLimit: baseGasLimit.toString(),
|
||||||
|
gasPrice: gasPrice.toString(),
|
||||||
|
estimatedGas: estimatedGas.toString(),
|
||||||
|
estimatedGasFee: estimatedGasFee.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return estimatedGasFee;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error estimating gas fee:', error);
|
||||||
|
// Return a conservative estimate if estimation fails
|
||||||
|
return parseEther("0.01"); // 0.01 ETH as fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback RPC configuration
|
// Fallback RPC configuration
|
||||||
const FALLBACK_RPC_URL = "https://radial-shy-cherry.arbitrum-mainnet.quiknode.pro/098e57e961b05b24bcde008c4ca02fff6fb13b51/";
|
const FALLBACK_RPC_URL = "https://radial-shy-cherry.arbitrum-mainnet.quiknode.pro/098e57e961b05b24bcde008c4ca02fff6fb13b51/";
|
||||||
const PRIMARY_RPC_URL = "https://arb1.arbitrum.io/rpc";
|
const PRIMARY_RPC_URL = "https://arb1.arbitrum.io/rpc";
|
||||||
@@ -518,6 +631,17 @@ export const openGmxPositionImpl = async (
|
|||||||
direction: direction
|
direction: direction
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check gas fees before opening position
|
||||||
|
console.log('⛽ Checking gas fees before opening position...');
|
||||||
|
const estimatedGasFee = await estimatePositionGasFee(sdk, params);
|
||||||
|
const gasFeeCheck = await checkGasFeeBalance(sdk, estimatedGasFee);
|
||||||
|
|
||||||
|
if (!gasFeeCheck.hasSufficientBalance) {
|
||||||
|
throw new Error(gasFeeCheck.errorMessage || 'Insufficient ETH balance for gas fees');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Gas fee check passed, proceeding with position opening...');
|
||||||
|
|
||||||
console.log('🚀 Executing position order...');
|
console.log('🚀 Executing position order...');
|
||||||
|
|
||||||
if (direction === TradeDirection.Long) {
|
if (direction === TradeDirection.Long) {
|
||||||
@@ -596,6 +720,28 @@ export async function openGmxPosition(
|
|||||||
hash
|
hash
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Handle gas fee specific errors
|
||||||
|
if (error instanceof Error && error.message.includes('Gas fee too high')) {
|
||||||
|
reply.status(400);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
errorType: 'GAS_FEE_TOO_HIGH',
|
||||||
|
suggestion: 'Please wait for lower gas fees or add more ETH to your wallet.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle insufficient ETH balance errors
|
||||||
|
if (error instanceof Error && error.message.includes('Insufficient ETH balance')) {
|
||||||
|
reply.status(400);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
errorType: 'INSUFFICIENT_ETH_BALANCE',
|
||||||
|
suggestion: 'Please add more ETH to your wallet to cover gas fees.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return handleError(this, reply, error, 'gmx/open-position');
|
return handleError(this, reply, error, 'gmx/open-position');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user