From e0795677e41c12a6bcb6f3ba0697b65e47307aa1 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 7 Nov 2025 23:46:48 +0700 Subject: [PATCH] Add whitelisting and admin --- src/Managing.Api/appsettings.Oda.json | 1 + .../Services/IWhitelistService.cs | 1 + .../Shared/AdminConfigurationService.cs | 39 +- src/Managing.Application/Users/UserService.cs | 17 +- .../Whitelist/WhitelistService.cs | 16 +- src/Managing.Domain/Users/User.cs | 2 + ...251107163433_AddIsAdminToUsers.Designer.cs | 1605 +++++++++++++++++ .../20251107163433_AddIsAdminToUsers.cs | 29 + .../ManagingDbContextModelSnapshot.cs | 3 + .../PostgreSql/Entities/UserEntity.cs | 1 + .../PostgreSql/PostgreSqlMappers.cs | 4 +- .../PostgreSql/PostgreSqlUserRepository.cs | 1 + .../src/generated/ManagingApi.ts | 185 ++ .../src/generated/ManagingApiTypes.ts | 49 + .../src/pages/settingsPage/settings.tsx | 6 + .../whitelist/whitelistSettings.tsx | 115 ++ .../settingsPage/whitelist/whitelistTable.tsx | 216 +++ 17 files changed, 2280 insertions(+), 10 deletions(-) create mode 100644 src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.Designer.cs create mode 100644 src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.cs create mode 100644 src/Managing.WebApp/src/pages/settingsPage/whitelist/whitelistSettings.tsx create mode 100644 src/Managing.WebApp/src/pages/settingsPage/whitelist/whitelistTable.tsx diff --git a/src/Managing.Api/appsettings.Oda.json b/src/Managing.Api/appsettings.Oda.json index bb51e372..c076fac7 100644 --- a/src/Managing.Api/appsettings.Oda.json +++ b/src/Managing.Api/appsettings.Oda.json @@ -34,6 +34,7 @@ "RequestsChannelId": 1018589494968078356, "ButtonExpirationMinutes": 2 }, + "EnableSwagger": true, "RunOrleansGrains": true, "AllowedHosts": "*", "KAIGEN_SECRET_KEY": "KaigenXCowchain", diff --git a/src/Managing.Application.Abstractions/Services/IWhitelistService.cs b/src/Managing.Application.Abstractions/Services/IWhitelistService.cs index 2a15e597..b447a1dc 100644 --- a/src/Managing.Application.Abstractions/Services/IWhitelistService.cs +++ b/src/Managing.Application.Abstractions/Services/IWhitelistService.cs @@ -13,5 +13,6 @@ public interface IWhitelistService Task SetIsWhitelistedAsync(IEnumerable accountIds, bool isWhitelisted); Task GetByIdAsync(int id); Task ProcessPrivyWebhookAsync(string privyUserId, long privyCreatedAt, string walletAddress, string? externalEthereumAccount, string? twitterAccount); + Task IsEmbeddedWalletWhitelistedAsync(string embeddedWallet); } diff --git a/src/Managing.Application/Shared/AdminConfigurationService.cs b/src/Managing.Application/Shared/AdminConfigurationService.cs index 7ba96579..b850ec14 100644 --- a/src/Managing.Application/Shared/AdminConfigurationService.cs +++ b/src/Managing.Application/Shared/AdminConfigurationService.cs @@ -1,4 +1,6 @@ +using Managing.Application.Abstractions.Repositories; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Managing.Application.Shared; @@ -13,11 +15,16 @@ public class AdminConfigurationService : IAdminConfigurationService { private readonly IConfiguration _configuration; private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; - public AdminConfigurationService(IConfiguration configuration, ILogger logger) + public AdminConfigurationService( + IConfiguration configuration, + ILogger logger, + IServiceScopeFactory serviceScopeFactory) { _configuration = configuration; _logger = logger; + _serviceScopeFactory = serviceScopeFactory; } public bool IsUserAdmin(string userName) @@ -27,15 +34,37 @@ public class AdminConfigurationService : IAdminConfigurationService return false; } + // First check configuration (for backward compatibility) var adminUserNames = GetAdminUserNames(); - var isAdmin = adminUserNames.Contains(userName, StringComparer.OrdinalIgnoreCase); + var isAdminFromConfig = adminUserNames.Contains(userName, StringComparer.OrdinalIgnoreCase); - if (isAdmin) + if (isAdminFromConfig) { - _logger.LogInformation("User {UserName} has admin privileges", userName); + _logger.LogInformation("User {UserName} has admin privileges from configuration", userName); + return true; + } + + // If not in config, check database User.IsAdmin flag + try + { + using var scope = _serviceScopeFactory.CreateScope(); + var userRepository = scope.ServiceProvider.GetRequiredService(); + + var user = userRepository.GetUserByNameAsync(userName).GetAwaiter().GetResult(); + + if (user != null && user.IsAdmin) + { + _logger.LogInformation("User {UserName} has admin privileges from database", userName); + return true; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error checking admin status for user {UserName} from database", userName); + // If database check fails, fall back to config-only result } - return isAdmin; + return false; } public List GetAdminUserNames() diff --git a/src/Managing.Application/Users/UserService.cs b/src/Managing.Application/Users/UserService.cs index ce14fade..0a4f5986 100644 --- a/src/Managing.Application/Users/UserService.cs +++ b/src/Managing.Application/Users/UserService.cs @@ -18,6 +18,7 @@ public class UserService : IUserService private readonly ILogger _logger; private readonly ICacheService _cacheService; private readonly IGrainFactory _grainFactory; + private readonly IWhitelistService _whitelistService; private readonly string[] _authorizedAddresses; public UserService( @@ -27,6 +28,7 @@ public class UserService : IUserService ILogger logger, ICacheService cacheService, IGrainFactory grainFactory, + IWhitelistService whitelistService, IConfiguration configuration) { _evmManager = evmManager; @@ -35,6 +37,7 @@ public class UserService : IUserService _logger = logger; _cacheService = cacheService; _grainFactory = grainFactory; + _whitelistService = whitelistService; var authorizedAddressesString = configuration["AUTHORIZED_ADDRESSES"] ?? string.Empty; _authorizedAddresses = string.IsNullOrEmpty(authorizedAddressesString) @@ -54,9 +57,19 @@ public class UserService : IUserService $"Message not good : {message} - Address : {address} - User : {name} - Signature : {signature}"); } - if (!_authorizedAddresses.Any(a => string.Equals(a, recoveredAddress, StringComparison.OrdinalIgnoreCase))) + // Check if address is whitelisted first (skip authorized addresses check if whitelisted) + var isWhitelisted = await _whitelistService.IsEmbeddedWalletWhitelistedAsync(recoveredAddress); + + if (!isWhitelisted) { - throw new Exception($"Address {recoveredAddress} not authorized. Please wait for team approval."); + // Only check authorized addresses if not whitelisted + var isInAuthorizedAddresses = _authorizedAddresses.Any(a => string.Equals(a, recoveredAddress, StringComparison.OrdinalIgnoreCase)); + + if (!isInAuthorizedAddresses) + { + _logger.LogWarning("Address {Address} is not authorized and not whitelisted", recoveredAddress); + throw new Exception($"Address {recoveredAddress} not authorized. Please wait for team approval."); + } } diff --git a/src/Managing.Application/Whitelist/WhitelistService.cs b/src/Managing.Application/Whitelist/WhitelistService.cs index bf86f55d..1ba8eab3 100644 --- a/src/Managing.Application/Whitelist/WhitelistService.cs +++ b/src/Managing.Application/Whitelist/WhitelistService.cs @@ -66,6 +66,18 @@ public class WhitelistService : IWhitelistService return await _whitelistRepository.GetByIdAsync(id); } + public async Task IsEmbeddedWalletWhitelistedAsync(string embeddedWallet) + { + if (string.IsNullOrWhiteSpace(embeddedWallet)) + { + return false; + } + + var account = await _whitelistRepository.GetByEmbeddedWalletAsync(embeddedWallet); + + return account?.IsWhitelisted ?? false; + } + public async Task ProcessPrivyWebhookAsync( string privyUserId, long privyCreatedAt, @@ -76,8 +88,8 @@ public class WhitelistService : IWhitelistService _logger.LogInformation("Processing Privy webhook - PrivyId: {PrivyId}, Wallet: {Wallet}, ExternalEthereum: {ExternalEthereum}, Twitter: {Twitter}", privyUserId, walletAddress, externalEthereumAccount ?? "null", twitterAccount ?? "null"); - // Convert Unix timestamp to DateTime - var privyCreationDate = DateTimeOffset.FromUnixTimeSeconds(privyCreatedAt).DateTime; + // Convert Unix timestamp to UTC DateTime (PostgreSQL requires UTC) + var privyCreationDate = DateTimeOffset.FromUnixTimeSeconds(privyCreatedAt).UtcDateTime; // Check if account already exists var existing = await _whitelistRepository.GetByPrivyIdAsync(privyUserId) ?? diff --git a/src/Managing.Domain/Users/User.cs b/src/Managing.Domain/Users/User.cs index d5574130..c75049ab 100644 --- a/src/Managing.Domain/Users/User.cs +++ b/src/Managing.Domain/Users/User.cs @@ -17,4 +17,6 @@ public class User [Id(4)] public string AvatarUrl { get; set; } = string.Empty; [Id(5)] public string TelegramChannel { get; set; } = string.Empty; + + [Id(6)] public bool IsAdmin { get; set; } = false; } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.Designer.cs new file mode 100644 index 00000000..847f1b3c --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.Designer.cs @@ -0,0 +1,1605 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20251107163433_AddIsAdminToUsers")] + partial class AddIsAdminToUsers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BacktestCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalFees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName") + .IsUnique(); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorsCount") + .HasColumnType("integer"); + + b.Property("IndicatorsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitialBalance") + .HasColumnType("decimal(18,8)"); + + b.Property("MaxDrawdown") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("MaxDrawdownRecoveryTime") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SharpeRatio") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("Ticker") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Timeframe") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Name"); + + b.HasIndex("UserId", "Score"); + + b.HasIndex("UserId", "Ticker"); + + b.HasIndex("UserId", "Timeframe"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("AccumulatedRunTimeSeconds") + .HasColumnType("bigint"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LastStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStopTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DateTimeRangesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("MoneyManagementVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TickerVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UniversalConfigJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.HasIndex("UserId", "Name", "Version"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("GasFees") + .HasColumnType("decimal(18,8)"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("NetPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UiFees") + .HasColumnType("decimal(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WhitelistAccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmbeddedWallet") + .IsRequired() + .HasMaxLength(42) + .HasColumnType("character varying(42)"); + + b.Property("ExternalEthereumAccount") + .HasMaxLength(42) + .HasColumnType("character varying(42)"); + + b.Property("IsWhitelisted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PrivyCreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivyId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TwitterAccount") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EmbeddedWallet") + .IsUnique(); + + b.HasIndex("ExternalEthereumAccount"); + + b.HasIndex("PrivyId") + .IsUnique(); + + b.HasIndex("TwitterAccount"); + + b.ToTable("WhitelistAccounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Navigation("Accounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.cs b/src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.cs new file mode 100644 index 00000000..82ca0610 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class AddIsAdminToUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsAdmin", + table: "Users", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsAdmin", + table: "Users"); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index 504326ca..40e35737 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -1306,6 +1306,9 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(500) .HasColumnType("character varying(500)"); + b.Property("IsAdmin") + .HasColumnType("boolean"); + b.Property("Name") .IsRequired() .HasMaxLength(255) diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs index 7e8e920d..c9bb6f11 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/UserEntity.cs @@ -13,6 +13,7 @@ public class UserEntity [MaxLength(255)] public string? AgentName { get; set; } public string? AvatarUrl { get; set; } public string? TelegramChannel { get; set; } + public bool IsAdmin { get; set; } = false; // Navigation properties public virtual ICollection Accounts { get; set; } = new List(); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index eb141a01..ff5c40de 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -131,6 +131,7 @@ public static class PostgreSqlMappers AvatarUrl = entity.AvatarUrl, TelegramChannel = entity.TelegramChannel, Id = entity.Id, // Assuming Id is the primary key for UserEntity + IsAdmin = entity.IsAdmin, Accounts = entity.Accounts?.Select(MapAccountWithoutUser).ToList() ?? new List() }; } @@ -163,7 +164,8 @@ public static class PostgreSqlMappers Name = user.Name, AgentName = user.AgentName, AvatarUrl = user.AvatarUrl, - TelegramChannel = user.TelegramChannel + TelegramChannel = user.TelegramChannel, + IsAdmin = user.IsAdmin }; } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs index 6bba3560..438def90 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs @@ -201,6 +201,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito existingUser.AgentName = user.AgentName; existingUser.AvatarUrl = user.AvatarUrl; existingUser.TelegramChannel = user.TelegramChannel; + existingUser.IsAdmin = user.IsAdmin; _context.Users.Update(existingUser); diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index c433713d..0c7de54f 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -3879,6 +3879,142 @@ export class UserClient extends AuthorizedApiBase { } } +export class WhitelistClient extends AuthorizedApiBase { + private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + super(configuration); + this.http = http ? http : window as any; + this.baseUrl = baseUrl ?? "http://localhost:5000"; + } + + whitelist_GetWhitelistAccounts(pageNumber: number | undefined, pageSize: number | undefined, searchExternalEthereumAccount: string | null | undefined, searchTwitterAccount: string | null | undefined): Promise { + let url_ = this.baseUrl + "/api/Whitelist?"; + if (pageNumber === null) + throw new Error("The parameter 'pageNumber' cannot be null."); + else if (pageNumber !== undefined) + url_ += "pageNumber=" + encodeURIComponent("" + pageNumber) + "&"; + if (pageSize === null) + throw new Error("The parameter 'pageSize' cannot be null."); + else if (pageSize !== undefined) + url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&"; + if (searchExternalEthereumAccount !== undefined && searchExternalEthereumAccount !== null) + url_ += "searchExternalEthereumAccount=" + encodeURIComponent("" + searchExternalEthereumAccount) + "&"; + if (searchTwitterAccount !== undefined && searchTwitterAccount !== null) + url_ += "searchTwitterAccount=" + encodeURIComponent("" + searchTwitterAccount) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processWhitelist_GetWhitelistAccounts(_response); + }); + } + + protected processWhitelist_GetWhitelistAccounts(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as PaginatedWhitelistAccountsResponse; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + whitelist_SetWhitelisted(id: number): Promise { + let url_ = this.baseUrl + "/api/Whitelist/{id}/whitelist"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "POST", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processWhitelist_SetWhitelisted(_response); + }); + } + + protected processWhitelist_SetWhitelisted(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as WhitelistAccount; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + whitelist_ProcessPrivyWebhook(webhook: PrivyWebhookDto): Promise { + let url_ = this.baseUrl + "/api/Whitelist/webhook"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(webhook); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processWhitelist_ProcessPrivyWebhook(_response); + }); + } + + protected processWhitelist_ProcessPrivyWebhook(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as WhitelistAccount; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } +} + export interface Account { id?: number; name: string; @@ -5117,6 +5253,55 @@ export interface LoginRequest { message: string; } +export interface PaginatedWhitelistAccountsResponse { + accounts?: WhitelistAccount[] | null; + totalCount?: number; + pageNumber?: number; + pageSize?: number; + totalPages?: number; +} + +export interface WhitelistAccount { + id?: number; + privyId?: string | null; + privyCreationDate?: Date; + embeddedWallet?: string | null; + externalEthereumAccount?: string | null; + twitterAccount?: string | null; + isWhitelisted?: boolean; + createdAt?: Date; + updatedAt?: Date | null; +} + +export interface PrivyWebhookDto { + type?: string | null; + user?: PrivyUserDto | null; + wallet?: PrivyWalletDto | null; +} + +export interface PrivyUserDto { + created_at?: number; + has_accepted_terms?: boolean; + id?: string | null; + is_guest?: boolean; + linked_accounts?: PrivyLinkedAccountDto[] | null; + mfa_methods?: any[] | null; +} + +export interface PrivyLinkedAccountDto { + address?: string | null; + first_verified_at?: number | null; + latest_verified_at?: number | null; + type?: string | null; + verified_at?: number | null; +} + +export interface PrivyWalletDto { + type?: string | null; + address?: string | null; + chain_type?: string | null; +} + export interface FileResponse { data: Blob; status: number; diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index daf59e5f..18abcb1d 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -1248,6 +1248,55 @@ export interface LoginRequest { message: string; } +export interface PaginatedWhitelistAccountsResponse { + accounts?: WhitelistAccount[] | null; + totalCount?: number; + pageNumber?: number; + pageSize?: number; + totalPages?: number; +} + +export interface WhitelistAccount { + id?: number; + privyId?: string | null; + privyCreationDate?: Date; + embeddedWallet?: string | null; + externalEthereumAccount?: string | null; + twitterAccount?: string | null; + isWhitelisted?: boolean; + createdAt?: Date; + updatedAt?: Date | null; +} + +export interface PrivyWebhookDto { + type?: string | null; + user?: PrivyUserDto | null; + wallet?: PrivyWalletDto | null; +} + +export interface PrivyUserDto { + created_at?: number; + has_accepted_terms?: boolean; + id?: string | null; + is_guest?: boolean; + linked_accounts?: PrivyLinkedAccountDto[] | null; + mfa_methods?: any[] | null; +} + +export interface PrivyLinkedAccountDto { + address?: string | null; + first_verified_at?: number | null; + latest_verified_at?: number | null; + type?: string | null; + verified_at?: number | null; +} + +export interface PrivyWalletDto { + type?: string | null; + address?: string | null; + chain_type?: string | null; +} + export interface FileResponse { data: Blob; status: number; diff --git a/src/Managing.WebApp/src/pages/settingsPage/settings.tsx b/src/Managing.WebApp/src/pages/settingsPage/settings.tsx index 335ee8c1..4bd08582 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/settings.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/settings.tsx @@ -10,6 +10,7 @@ import DefaultConfig from './defaultConfig/defaultConfig' import UserInfoSettings from './UserInfoSettings' import AccountFee from './accountFee/accountFee' import SqlMonitoring from './sqlmonitoring/sqlMonitoring' +import WhitelistSettings from './whitelist/whitelistSettings' type TabsType = { label: string @@ -59,6 +60,11 @@ const tabs: TabsType = [ index: 8, label: 'SQL Monitoring', }, + { + Component: WhitelistSettings, + index: 9, + label: 'Whitelist', + }, ] const Settings: React.FC = () => { diff --git a/src/Managing.WebApp/src/pages/settingsPage/whitelist/whitelistSettings.tsx b/src/Managing.WebApp/src/pages/settingsPage/whitelist/whitelistSettings.tsx new file mode 100644 index 00000000..5552c295 --- /dev/null +++ b/src/Managing.WebApp/src/pages/settingsPage/whitelist/whitelistSettings.tsx @@ -0,0 +1,115 @@ +import {useState} from 'react' +import {useQuery} from '@tanstack/react-query' + +import useApiUrlStore from '../../../app/store/apiStore' +import {WhitelistClient} from '../../../generated/ManagingApi' + +import WhitelistTable from './whitelistTable' + +const WhitelistSettings: React.FC = () => { + const { apiUrl } = useApiUrlStore() + const [pageNumber, setPageNumber] = useState(1) + const [pageSize, setPageSize] = useState(20) + const [searchExternalEthereumAccount, setSearchExternalEthereumAccount] = useState('') + const [searchTwitterAccount, setSearchTwitterAccount] = useState('') + + const whitelistClient = new WhitelistClient({}, apiUrl) + + const { + data: whitelistData, + isLoading, + error, + refetch + } = useQuery({ + queryKey: ['whitelistAccounts', pageNumber, pageSize, searchExternalEthereumAccount, searchTwitterAccount], + queryFn: async () => { + return await whitelistClient.whitelist_GetWhitelistAccounts( + pageNumber, + pageSize, + searchExternalEthereumAccount || null, + searchTwitterAccount || null + ) + }, + staleTime: 30000, + gcTime: 5 * 60 * 1000, + }) + + const accounts = whitelistData?.accounts || [] + const totalCount = whitelistData?.totalCount || 0 + const totalPages = whitelistData?.totalPages || 0 + + const handlePageChange = (newPage: number) => { + setPageNumber(newPage) + } + + const handleSearchExternalEthereum = (value: string) => { + setSearchExternalEthereumAccount(value) + setPageNumber(1) // Reset to first page when searching + } + + const handleSearchTwitter = (value: string) => { + setSearchTwitterAccount(value) + setPageNumber(1) // Reset to first page when searching + } + + const handleWhitelistSuccess = () => { + refetch() + } + + return ( +
+
+
+

Whitelist Accounts

+ + {/* Search Filters */} +
+
+ + handleSearchExternalEthereum(e.target.value)} + /> +
+
+ + handleSearchTwitter(e.target.value)} + /> +
+
+
+ + + + {error && ( +
+ Failed to load whitelist accounts. Please try again. +
+ )} +
+
+ ) +} + +export default WhitelistSettings + diff --git a/src/Managing.WebApp/src/pages/settingsPage/whitelist/whitelistTable.tsx b/src/Managing.WebApp/src/pages/settingsPage/whitelist/whitelistTable.tsx new file mode 100644 index 00000000..e769d7dc --- /dev/null +++ b/src/Managing.WebApp/src/pages/settingsPage/whitelist/whitelistTable.tsx @@ -0,0 +1,216 @@ +import React, {useMemo} from 'react' +import {type WhitelistAccount, WhitelistClient} from '../../../generated/ManagingApi' +import useApiUrlStore from '../../../app/store/apiStore' +import {Table, Toast} from '../../../components/mollecules' + +interface IWhitelistList { + list: WhitelistAccount[] + isFetching: boolean + totalCount: number + currentPage: number + totalPages: number + onPageChange: (page: number) => void + onWhitelistSuccess: () => void +} + +const WhitelistTable: React.FC = ({ + list, + isFetching, + totalCount, + currentPage, + totalPages, + onPageChange, + onWhitelistSuccess +}) => { + const { apiUrl } = useApiUrlStore() + const whitelistClient = new WhitelistClient({}, apiUrl) + + async function handleSetWhitelisted(id: number) { + const t = new Toast('Updating whitelist status...') + + try { + await whitelistClient.whitelist_SetWhitelisted(id) + t.update('success', 'Account whitelisted successfully') + onWhitelistSuccess() + } catch (err: any) { + const errorMessage = err?.response || err?.message || 'Unknown error' + t.update('error', `Error: ${errorMessage}`) + } + } + + const columns = useMemo(() => [ + { + Header: 'Is Whitelisted', + accessor: 'isWhitelisted', + width: 120, + Cell: ({ value }: any) => ( +
+ {value ? 'Yes' : 'No'} +
+ ), + }, + { + Header: 'Actions', + accessor: 'actions', + width: 150, + Cell: ({ row }: any) => { + const account = row.original as WhitelistAccount + return ( +
+ {!account.isWhitelisted && ( + + )} + {account.isWhitelisted && ( + Whitelisted + )} +
+ ) + }, + }, + { + Header: 'Privy ID', + accessor: 'privyId', + width: 200, + }, + { + Header: 'Embedded Wallet', + accessor: 'embeddedWallet', + width: 200, + Cell: ({ value }: any) => ( + {value || '-'} + ), + }, + { + Header: 'External Ethereum Account', + accessor: 'externalEthereumAccount', + width: 200, + Cell: ({ value }: any) => ( + {value || '-'} + ), + }, + { + Header: 'Twitter Account', + accessor: 'twitterAccount', + width: 150, + Cell: ({ value }: any) => ( + {value || '-'} + ), + }, + ], [isFetching]) + + const tableData = useMemo(() => { + return list.map((account) => ({ + id: account.id, + privyId: account.privyId || '-', + embeddedWallet: account.embeddedWallet || '-', + externalEthereumAccount: account.externalEthereumAccount || '-', + twitterAccount: account.twitterAccount || '-', + isWhitelisted: account.isWhitelisted || false, + createdAt: account.createdAt, + actions: account, + })) + }, [list]) + + return ( +
+
+

+ Total accounts: {totalCount} | Page {currentPage} of {totalPages} +

+
+ + {isFetching && ( +
+ +
+ )} + + {!isFetching && list.length === 0 && ( +
+ No whitelist accounts found. +
+ )} + + {!isFetching && list.length > 0 && ( + <> + + + {/* Manual Pagination */} + {totalPages > 1 && ( +
+ + + + {/* Page numbers */} +
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + let pageNum + if (totalPages <= 5) { + pageNum = i + 1 + } else if (currentPage <= 3) { + pageNum = i + 1 + } else if (currentPage >= totalPages - 2) { + pageNum = totalPages - 4 + i + } else { + pageNum = currentPage - 2 + i + } + + return ( + + ) + })} +
+ + + +
+ )} + + )} + + ) +} + +export default WhitelistTable +