Add validation for Kudai strategy staking requirements in StartCopyTradingCommandHandler. Implemented methods in IEvmManager to retrieve staked KUDAI balance and GBC NFT count. Enhanced error handling for staking checks.

This commit is contained in:
2025-11-17 12:57:47 +07:00
parent 4b24a934ad
commit 8697f1598d
4 changed files with 147 additions and 13 deletions

View File

@@ -93,4 +93,18 @@ public interface IEvmManager
DateTime? toDate = null,
int pageIndex = 0,
int pageSize = 20);
/// <summary>
/// Gets the staked KUDAI balance for a specific address
/// </summary>
/// <param name="address">The wallet address to check</param>
/// <returns>The amount of KUDAI tokens staked</returns>
Task<decimal> GetKudaiStakedBalance(string address);
/// <summary>
/// Gets the count of staked GBC NFTs for a specific address
/// </summary>
/// <param name="address">The wallet address to check</param>
/// <returns>The number of GBC NFTs staked</returns>
Task<int> GetGbcStakedCount(string address);
}

View File

@@ -1,13 +1,14 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using MediatR;
using static Managing.Common.Enums;
using System;
namespace Managing.Application.ManageBot
{
@@ -17,14 +18,16 @@ namespace Managing.Application.ManageBot
private readonly IGrainFactory _grainFactory;
private readonly IBotService _botService;
private readonly IKaigenService _kaigenService;
private readonly IEvmManager _evmManager;
public StartCopyTradingCommandHandler(
IAccountService accountService, IGrainFactory grainFactory, IBotService botService, IKaigenService kaigenService)
IAccountService accountService, IGrainFactory grainFactory, IBotService botService, IKaigenService kaigenService, IEvmManager evmManager)
{
_accountService = accountService;
_grainFactory = grainFactory;
_botService = botService;
_kaigenService = kaigenService;
_evmManager = evmManager;
}
public async Task<string> Handle(StartCopyTradingCommand request, CancellationToken cancellationToken)
@@ -43,17 +46,23 @@ namespace Managing.Application.ManageBot
throw new ArgumentException($"Master bot with identifier {request.MasterBotIdentifier} not found");
}
// Verify the user owns the keys of the master strategy
var ownedKeys = await _kaigenService.GetOwnedKeysAsync(request.User);
var hasMasterStrategyKey = ownedKeys.Items.Any(key =>
string.Equals(key.AgentName, request.MasterBotIdentifier.ToString(), StringComparison.OrdinalIgnoreCase) &&
key.Owned >= 1);
if (!hasMasterStrategyKey)
// Special validation for Kudai strategy - check staking requirements
if (string.Equals(request.MasterBotIdentifier.ToString(), "Kudai", StringComparison.OrdinalIgnoreCase))
{
throw new UnauthorizedAccessException(
$"You don't own the keys for the master strategy '{request.MasterBotIdentifier}'. " +
"You must own at least 1 key for this strategy to copy trade from it.");
await ValidateKudaiStakingRequirements(request.User);
}else {
// Verify the user owns the keys of the master strategy
var ownedKeys = await _kaigenService.GetOwnedKeysAsync(request.User);
var hasMasterStrategyKey = ownedKeys.Items.Any(key =>
string.Equals(key.AgentName, request.MasterBotIdentifier.ToString(), StringComparison.OrdinalIgnoreCase) &&
key.Owned >= 1);
if (!hasMasterStrategyKey)
{
throw new UnauthorizedAccessException(
$"You don't own the keys for the master strategy '{request.MasterBotIdentifier}'. " +
"You must own at least 1 key for this strategy to copy trade from it.");
}
}
// Get the master bot configuration
@@ -153,5 +162,39 @@ namespace Managing.Application.ManageBot
throw new Exception($"Failed to start copy trading bot: {ex.Message}, {ex.StackTrace}");
}
}
private async Task ValidateKudaiStakingRequirements(User user)
{
// Get user's accounts to find their wallet address
var userAccounts = await _accountService.GetAccountsByUserAsync(user, true, true);
var evmAccount = userAccounts.FirstOrDefault(acc =>
acc.Exchange == TradingExchanges.Evm ||
acc.Exchange == TradingExchanges.GmxV2);
if (evmAccount == null)
{
throw new InvalidOperationException(
"To copy trade the Kudai strategy, you must have an EVM-compatible account (GMX V2 or EVM exchange).");
}
// Check KUDAI staked balance
var kudaiStakedBalance = await _evmManager.GetKudaiStakedBalance(evmAccount.Key);
// Check GBC staked NFT count
var gbcStakedCount = await _evmManager.GetGbcStakedCount(evmAccount.Key);
// Requirements: 100 million KUDAI OR 10 GBC NFTs
const decimal requiredKudaiAmount = 100_000_000m; // 100 million
const int requiredGbcCount = 10;
if (kudaiStakedBalance < requiredKudaiAmount && gbcStakedCount < requiredGbcCount)
{
throw new UnauthorizedAccessException(
$"To copy trade the Kudai strategy, you must have either:\n" +
$"• {requiredKudaiAmount:N0} KUDAI tokens staked (currently have {kudaiStakedBalance:N0})\n" +
$"• OR {requiredGbcCount} GBC NFTs staked (currently have {gbcStakedCount})\n\n" +
$"Please stake the required amount in the Kudai staking contract to copy this strategy.");
}
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Numerics;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
namespace Managing.Infrastructure.Evm.Contracts;
public class TokenStakesFunction : FunctionMessage
{
[Parameter("address", "_user", 1)]
public string User { get; set; }
}
public class TokenStakesOutputDTO : FunctionOutputDTO
{
[Parameter("uint256", "", 1)]
public BigInteger ReturnValue1 { get; set; }
}
public class GetUserStakedNftsFunction : FunctionMessage
{
[Parameter("address", "_user", 1)]
public string User { get; set; }
}
public class GetUserStakedNftsOutputDTO : FunctionOutputDTO
{
[Parameter("uint256[]", "", 1)]
public List<BigInteger> ReturnValue1 { get; set; }
}

View File

@@ -11,6 +11,7 @@ using Managing.Domain.Evm;
using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using Managing.Infrastructure.Evm.Abstractions;
using Managing.Infrastructure.Evm.Contracts;
using Managing.Infrastructure.Evm.Models;
using Managing.Infrastructure.Evm.Models.Gmx.v2;
using Managing.Infrastructure.Evm.Models.Privy;
@@ -43,6 +44,10 @@ public class EvmManager : IEvmManager
private readonly IWeb3ProxyService _web3ProxyService;
private readonly ICacheService _cacheService;
// Kudai staking contract constants
private const string KudaiTokenAddress = "0xc9bD1C1e65eBFb36cF4b3d9FC8E2b844248deEE8";
private const string StackingContractAddress = "0x5b02047D471cFf479EBD37575e41Eaa37a4d9321";
public EvmManager(IEnumerable<ISubgraphPrices> subgraphs,
IWeb3ProxyService web3ProxyService, ICacheService cacheService)
{
@@ -996,4 +1001,47 @@ public class EvmManager : IEvmManager
// Map the result to the Position domain object
return result;
}
public async Task<decimal> GetKudaiStakedBalance(string address)
{
try
{
var tokenStakesMessage = new TokenStakesFunction() { User = address };
var queryHandler = _web3.Eth.GetContractQueryHandler<TokenStakesFunction>();
var stakedAmount = await queryHandler
.QueryAsync<TokenStakesOutputDTO>(StackingContractAddress, tokenStakesMessage)
.ConfigureAwait(false);
// KUDAI has 18 decimals like most ERC20 tokens
return Web3.Convert.FromWei(stakedAmount.ReturnValue1, 18);
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex);
Console.Error.WriteLine($"Error getting KUDAI staked balance for address {address}: {ex.Message}");
return 0;
}
}
public async Task<int> GetGbcStakedCount(string address)
{
try
{
var getUserStakedNftsMessage = new GetUserStakedNftsFunction() { User = address };
var queryHandler = _web3.Eth.GetContractQueryHandler<GetUserStakedNftsFunction>();
var stakedNfts = await queryHandler
.QueryAsync<GetUserStakedNftsOutputDTO>(StackingContractAddress, getUserStakedNftsMessage)
.ConfigureAwait(false);
return stakedNfts.ReturnValue1?.Count ?? 0;
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex);
Console.Error.WriteLine($"Error getting GBC staked count for address {address}: {ex.Message}");
return 0;
}
}
}