Add ETH and USDC balance check before start/restart bot and autoswap

This commit is contained in:
2025-09-23 14:03:46 +07:00
parent d13ac9fd21
commit 40f3c66694
23 changed files with 847 additions and 284 deletions

View File

@@ -891,11 +891,15 @@ public class EvmManager : IEvmManager
public async Task<decimal> GetFee(string chainName)
{
var chain = ChainService.GetChain(chainName);
var web3 = new Web3(chain.RpcUrl);
var etherPrice = (await GetPrices(new List<string> { "ethereum" }))["ethereum"]["usd"];
var fee = await GmxService.GetFee(web3, etherPrice);
return fee;
try
{
return await _web3ProxyService.GetEstimatedGasFeeUsdAsync();
}
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)

View File

@@ -13,7 +13,7 @@ namespace Managing.Infrastructure.Evm.Models.Proxy
/// </summary>
[JsonPropertyName("success")]
public bool Success { get; set; }
/// <summary>
/// Error message if not successful
/// </summary>
@@ -44,25 +44,25 @@ namespace Managing.Infrastructure.Evm.Models.Proxy
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }
/// <summary>
/// Error message
/// </summary>
[JsonPropertyName("message")]
public string Message { get; set; }
/// <summary>
/// Error stack trace
/// </summary>
[JsonPropertyName("stack")]
public string Stack { get; set; }
/// <summary>
/// HTTP status code (added by service)
/// </summary>
[JsonIgnore]
public int StatusCode { get; set; }
/// <summary>
/// Returns a formatted error message with type and message
/// </summary>
@@ -90,7 +90,7 @@ namespace Managing.Infrastructure.Evm.Models.Proxy
/// The error details from the API
/// </summary>
public Web3ProxyError Error { get; }
/// <summary>
/// Simple error message from API
/// </summary>
@@ -100,12 +100,12 @@ namespace Managing.Infrastructure.Evm.Models.Proxy
/// Creates a new Web3ProxyException from a structured error
/// </summary>
/// <param name="error">The error details</param>
public Web3ProxyException(Web3ProxyError error)
public Web3ProxyException(Web3ProxyError error)
: base(error?.Message ?? "An error occurred in the Web3Proxy API")
{
Error = error;
}
/// <summary>
/// Creates a new Web3ProxyException from a simple error message
/// </summary>
@@ -121,7 +121,7 @@ namespace Managing.Infrastructure.Evm.Models.Proxy
/// </summary>
/// <param name="message">Custom error message</param>
/// <param name="error">The error details</param>
public Web3ProxyException(string message, Web3ProxyError error)
public Web3ProxyException(string message, Web3ProxyError error)
: base(message)
{
Error = error;
@@ -151,4 +151,28 @@ namespace Managing.Infrastructure.Evm.Models.Proxy
[JsonPropertyName("balances")]
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; }
}
}

View File

@@ -5,6 +5,7 @@ using System.Text;
using System.Text.Json;
using System.Web;
using Managing.Application.Abstractions.Services;
using Managing.Core.Exceptions;
using Managing.Domain.Accounts;
using Managing.Infrastructure.Evm.Models.Proxy;
using Microsoft.Extensions.Logging;
@@ -77,6 +78,64 @@ namespace Managing.Infrastructure.Evm.Services
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)
{
try
@@ -91,6 +150,11 @@ namespace Managing.Infrastructure.Evm.Services
var result = await response.Content.ReadFromJsonAsync<T>(_jsonOptions);
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))
{
_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);
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))
{
_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>();
}
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)
{
var statusCode = (int)response.StatusCode;
@@ -337,6 +423,13 @@ namespace Managing.Infrastructure.Evm.Services
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
throw new Web3ProxyException(errorResponse.Error);
}
@@ -351,20 +444,53 @@ namespace Managing.Infrastructure.Evm.Services
if (structuredErrorResponse?.ErrorDetails != null)
{
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);
}
}
catch (Exception ex) when (ex is InsufficientFundsException)
{
// Re-throw insufficient funds exceptions as-is
throw;
}
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
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))
{
SentrySdk.CaptureException(ex);
// If we couldn't parse the error as JSON or another issue occurred
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}");
}
}