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

@@ -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}");
}
}