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