Add single time swap + fetch balance cache in AgentGrain

This commit is contained in:
2025-09-21 23:41:27 +07:00
parent 8afe80ca0e
commit 6aad2834a9
5 changed files with 369 additions and 59 deletions

View File

@@ -4,6 +4,7 @@ using Managing.Application.Abstractions.Models;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots.Models;
using Managing.Application.Orleans;
using Managing.Common;
using Managing.Domain.Statistics;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
@@ -233,4 +234,242 @@ public class AgentGrain : Grain, IAgentGrain
await UpdateSummary();
}
}
public async Task<BalanceCheckResult> CheckAndEnsureEthBalanceAsync(Guid requestingBotId, string accountName)
{
try
{
// Check if a swap is already in progress
if (_state.State.IsSwapInProgress)
{
_logger.LogInformation("Swap already in progress for agent {UserId}, bot {RequestingBotId} will wait",
this.GetPrimaryKeyLong(), requestingBotId);
return new BalanceCheckResult
{
IsSuccessful = false,
FailureReason = BalanceCheckFailureReason.SwapInProgress,
Message = "Swap operation already in progress",
ShouldStopBot = false
};
}
// Check cooldown period (5 minutes between swaps)
if (_state.State.LastSwapTime.HasValue &&
DateTime.UtcNow - _state.State.LastSwapTime.Value < TimeSpan.FromMinutes(5))
{
_logger.LogInformation("Swap cooldown period active for agent {UserId}, bot {RequestingBotId} will wait",
this.GetPrimaryKeyLong(), requestingBotId);
return new BalanceCheckResult
{
IsSuccessful = false,
FailureReason = BalanceCheckFailureReason.SwapCooldownActive,
Message = "Swap cooldown period active",
ShouldStopBot = false
};
}
// Get or refresh cached balance data
var balanceData = await GetOrRefreshBalanceDataAsync(accountName);
if (balanceData == null)
{
_logger.LogError("Failed to get balance data for account {AccountName}, user {UserId}",
accountName, this.GetPrimaryKeyLong());
return new BalanceCheckResult
{
IsSuccessful = false,
FailureReason = BalanceCheckFailureReason.BalanceFetchError,
Message = "Failed to fetch balance data",
ShouldStopBot = false
};
}
_logger.LogInformation("Agent {UserId} balance check - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (cached: {IsCached})",
this.GetPrimaryKeyLong(), balanceData.EthValueInUsd, balanceData.UsdcValue,
_state.State.CachedBalanceData?.IsValid == true);
// Check USDC minimum balance first (this will stop the bot if insufficient)
if (balanceData.UsdcValue < Constants.GMX.Config.MinimumPositionAmount)
{
_logger.LogWarning("USDC balance is below minimum required amount - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (minimum: {Minimum})",
balanceData.EthValueInUsd, balanceData.UsdcValue, Constants.GMX.Config.MinimumPositionAmount);
return new BalanceCheckResult
{
IsSuccessful = false,
FailureReason = BalanceCheckFailureReason.InsufficientUsdcBelowMinimum,
Message = $"USDC balance below minimum required amount ({Constants.GMX.Config.MinimumPositionAmount} USD)",
ShouldStopBot = true
};
}
// If ETH balance is sufficient, return success
if (balanceData.EthValueInUsd >= Constants.GMX.Config.MinimumEthBalance)
{
return new BalanceCheckResult
{
IsSuccessful = true,
FailureReason = BalanceCheckFailureReason.None,
Message = "Balance check successful",
ShouldStopBot = false
};
}
// Check if we have enough USDC for swap (need at least 5 USD for swap)
if (balanceData.UsdcValue < (Constants.GMX.Config.MinimumPositionAmount + Constants.GMX.Config.AutoSwapAmount) )
{
_logger.LogWarning("Insufficient USDC balance for swap - ETH: {EthValue:F2} USD, USDC: {UsdcValue:F2} USD (need {AutoSwapAmount} USD for swap)",
balanceData.EthValueInUsd, balanceData.UsdcValue, Constants.GMX.Config.AutoSwapAmount);
return new BalanceCheckResult
{
IsSuccessful = false,
FailureReason = BalanceCheckFailureReason.InsufficientUsdcForSwap,
Message = $"Insufficient USDC balance for swap (need {Constants.GMX.Config.AutoSwapAmount} USD)",
ShouldStopBot = true
};
}
// Mark swap as in progress
_state.State.IsSwapInProgress = true;
await _state.WriteStateAsync();
try
{
_logger.LogInformation("Initiating USDC to ETH swap for agent {UserId} - swapping 5 USDC", this.GetPrimaryKeyLong());
// Get user for the swap
var userId = (int)this.GetPrimaryKeyLong();
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
var swapInfo = await _accountService.SwapGmxTokensAsync(user, accountName,
Ticker.USDC, Ticker.ETH, 5);
if (swapInfo.Success)
{
_logger.LogInformation("Successfully swapped 5 USDC to ETH for agent {UserId}, transaction hash: {Hash}",
userId, swapInfo.Hash);
// Update last swap time and invalidate cache
_state.State.LastSwapTime = DateTime.UtcNow;
_state.State.CachedBalanceData = null; // Invalidate cache after successful swap
return new BalanceCheckResult
{
IsSuccessful = true,
FailureReason = BalanceCheckFailureReason.None,
Message = "Swap completed successfully",
ShouldStopBot = false
};
}
else
{
_logger.LogError("Failed to swap USDC to ETH for agent {UserId}: {Error}",
userId, swapInfo.Error ?? swapInfo.Message);
return new BalanceCheckResult
{
IsSuccessful = false,
FailureReason = BalanceCheckFailureReason.SwapExecutionError,
Message = swapInfo.Error ?? swapInfo.Message ?? "Swap execution failed",
ShouldStopBot = false
};
}
}
finally
{
// Always clear the swap in progress flag
_state.State.IsSwapInProgress = false;
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>
/// Gets cached balance data or fetches fresh data if cache is invalid/expired
/// </summary>
private async Task<CachedBalanceData?> GetOrRefreshBalanceDataAsync(string accountName)
{
try
{
// Check if we have valid cached data for the same account
if (_state.State.CachedBalanceData?.IsValid == true &&
_state.State.CachedBalanceData.AccountName == accountName)
{
_logger.LogDebug("Using cached balance data for account {AccountName}", accountName);
return _state.State.CachedBalanceData;
}
// Fetch fresh balance data
_logger.LogInformation("Fetching fresh balance data for account {AccountName}", accountName);
var userId = (int)this.GetPrimaryKeyLong();
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
_logger.LogError("User {UserId} not found for balance check", userId);
return null;
}
var userAccounts = await _accountService.GetAccountsByUserAsync(user, hideSecrets: true, true);
var account = userAccounts.FirstOrDefault(a => a.Name == accountName);
if (account == null)
{
_logger.LogError("Account {AccountName} not found for user {UserId}", accountName, userId);
return null;
}
// 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;
// Cache the balance data
var balanceData = new CachedBalanceData
{
LastFetched = DateTime.UtcNow,
AccountName = accountName,
EthValueInUsd = ethValueInUsd,
UsdcValue = usdcValue
};
_state.State.CachedBalanceData = balanceData;
await _state.WriteStateAsync();
return balanceData;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching balance data for account {AccountName}, user {UserId}",
accountName, this.GetPrimaryKeyLong());
return null;
}
}
}