From 8697f1598da221f47ef6407b5c0de3b20933f425 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 17 Nov 2025 12:57:47 +0700 Subject: [PATCH] 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. --- .../Repositories/IEvmManager.cs | 14 ++++ .../StartCopyTradingCommandHandler.cs | 69 +++++++++++++++---- .../Contracts/KudaiStakingContracts.cs | 29 ++++++++ .../EvmManager.cs | 48 +++++++++++++ 4 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 src/Managing.Infrastructure.Web3/Contracts/KudaiStakingContracts.cs diff --git a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs index dbbdd2f7..627a7012 100644 --- a/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs +++ b/src/Managing.Application.Abstractions/Repositories/IEvmManager.cs @@ -93,4 +93,18 @@ public interface IEvmManager DateTime? toDate = null, int pageIndex = 0, int pageSize = 20); + + /// + /// Gets the staked KUDAI balance for a specific address + /// + /// The wallet address to check + /// The amount of KUDAI tokens staked + Task GetKudaiStakedBalance(string address); + + /// + /// Gets the count of staked GBC NFTs for a specific address + /// + /// The wallet address to check + /// The number of GBC NFTs staked + Task GetGbcStakedCount(string address); } \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs b/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs index d9cb9686..07129234 100644 --- a/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs +++ b/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs @@ -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 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."); + } + } } -} +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Contracts/KudaiStakingContracts.cs b/src/Managing.Infrastructure.Web3/Contracts/KudaiStakingContracts.cs new file mode 100644 index 00000000..9159e259 --- /dev/null +++ b/src/Managing.Infrastructure.Web3/Contracts/KudaiStakingContracts.cs @@ -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 ReturnValue1 { get; set; } +} diff --git a/src/Managing.Infrastructure.Web3/EvmManager.cs b/src/Managing.Infrastructure.Web3/EvmManager.cs index 4500536e..85b8e4cf 100644 --- a/src/Managing.Infrastructure.Web3/EvmManager.cs +++ b/src/Managing.Infrastructure.Web3/EvmManager.cs @@ -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 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 GetKudaiStakedBalance(string address) + { + try + { + var tokenStakesMessage = new TokenStakesFunction() { User = address }; + var queryHandler = _web3.Eth.GetContractQueryHandler(); + + var stakedAmount = await queryHandler + .QueryAsync(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 GetGbcStakedCount(string address) + { + try + { + var getUserStakedNftsMessage = new GetUserStakedNftsFunction() { User = address }; + var queryHandler = _web3.Eth.GetContractQueryHandler(); + + var stakedNfts = await queryHandler + .QueryAsync(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; + } + } } \ No newline at end of file