diff --git a/src/Managing.Api/Authorization/JwtUtils.cs b/src/Managing.Api/Authorization/JwtUtils.cs index 85d8da9f..90c57aaf 100644 --- a/src/Managing.Api/Authorization/JwtUtils.cs +++ b/src/Managing.Api/Authorization/JwtUtils.cs @@ -24,7 +24,10 @@ public class JwtUtils : IJwtUtils _secret = config.GetValue("Jwt:Secret") ?? throw new InvalidOperationException("JWT secret is not configured."); _issuer = config.GetValue("Authentication:Schemes:Bearer:ValidIssuer"); - _audience = config.GetValue("Authentication:Schemes:Bearer:ValidAudiences"); + // Get first audience from array (tokens are generated with a single audience) + var audiences = config.GetSection("Authentication:Schemes:Bearer:ValidAudiences") + .Get() ?? Array.Empty(); + _audience = audiences.Length > 0 ? audiences[0] : null; } public string GenerateJwtToken(User user, string publicAddress) @@ -37,7 +40,7 @@ public class JwtUtils : IJwtUtils Subject = new ClaimsIdentity(new[] { new Claim("address", publicAddress) }), Expires = DateTime.UtcNow.AddDays(15), Issuer = _issuer, // Include issuer if configured - Audience = _audience, // Include audience if configured + Audience = _audience, // Include audience if configured (uses first from array) SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) diff --git a/src/Managing.Api/Controllers/WhitelistController.cs b/src/Managing.Api/Controllers/WhitelistController.cs new file mode 100644 index 00000000..fc1e7744 --- /dev/null +++ b/src/Managing.Api/Controllers/WhitelistController.cs @@ -0,0 +1,231 @@ +using Managing.Api.Models; +using Managing.Application.Abstractions.Services; +using Managing.Application.Shared; +using Managing.Domain.Whitelist; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Managing.Api.Controllers; + +/// +/// Controller for managing whitelist accounts (Privy wallets). +/// Requires admin authorization for all operations. +/// +[ApiController] +[Authorize] +[Route("api/[controller]")] +public class WhitelistController : BaseController +{ + private readonly IWhitelistService _whitelistService; + private readonly IAdminConfigurationService _adminService; + private readonly ILogger _logger; + + public WhitelistController( + IUserService userService, + IWhitelistService whitelistService, + IAdminConfigurationService adminService, + ILogger logger) + : base(userService) + { + _whitelistService = whitelistService; + _adminService = adminService; + _logger = logger; + } + + /// + /// Gets a paginated list of whitelist accounts with optional search filters. + /// + /// Page number (1-based) + /// Page size (1-100) + /// Optional search term for external Ethereum account + /// Optional search term for Twitter account + /// Paginated list of whitelist accounts + [HttpGet] + public async Task> GetWhitelistAccounts( + [FromQuery] int pageNumber = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? searchExternalEthereumAccount = null, + [FromQuery] string? searchTwitterAccount = null) + { + var user = await GetUser(); + + if (!_adminService.IsUserAdmin(user.Name)) + { + _logger.LogWarning("User {UserName} attempted to list whitelist accounts without admin privileges", user.Name); + return Forbid("Only admin users can list whitelist accounts"); + } + + try + { + var (accounts, totalCount) = await _whitelistService.GetPaginatedAsync( + pageNumber, + pageSize, + searchExternalEthereumAccount, + searchTwitterAccount); + + return Ok(new PaginatedWhitelistAccountsResponse + { + Accounts = accounts, + TotalCount = totalCount, + PageNumber = pageNumber, + PageSize = pageSize, + TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize) + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing whitelist accounts for admin user {UserName}", user.Name); + return StatusCode(500, new { error = ex.Message }); + } + } + + /// + /// Sets the IsWhitelisted status to true for a selected account. + /// + /// The ID of the account to whitelist + /// The updated account + [HttpPost("{id}/whitelist")] + public async Task> SetWhitelisted(int id) + { + var user = await GetUser(); + + if (!_adminService.IsUserAdmin(user.Name)) + { + _logger.LogWarning("User {UserName} attempted to set whitelisted status without admin privileges", user.Name); + return Forbid("Only admin users can set whitelisted status"); + } + + try + { + var updatedCount = await _whitelistService.SetIsWhitelistedAsync(new[] { id }, true); + + if (updatedCount == 0) + { + return NotFound(new { error = $"Whitelist account with Id {id} not found" }); + } + + _logger.LogInformation("Admin user {UserName} set IsWhitelisted=true for account {Id}", + user.Name, id); + + // Fetch the updated account to return it + var updatedAccount = await _whitelistService.GetByIdAsync(id); + + if (updatedAccount == null) + { + return NotFound(new { error = $"Whitelist account with Id {id} not found after update" }); + } + + return Ok(updatedAccount); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error setting whitelisted status for admin user {UserName}", user.Name); + return StatusCode(500, new { error = ex.Message }); + } + } + + /// + /// Receives Privy webhook for wallet creation events. + /// This endpoint should be called by Privy when a wallet is created. + /// + /// The Privy webhook payload + /// The created or updated whitelist account + [HttpPost("webhook")] + [AllowAnonymous] // Webhook endpoint - consider adding webhook signature verification + public async Task> ProcessPrivyWebhook( + [FromBody] PrivyWebhookDto webhook) + { + try + { + // Validate webhook type + if (webhook.Type != "user.wallet_created") + { + _logger.LogWarning("Received webhook with unexpected type: {Type}", webhook.Type); + return BadRequest(new { error = $"Unexpected webhook type: {webhook.Type}" }); + } + + // Validate required fields + if (string.IsNullOrWhiteSpace(webhook.User?.Id)) + { + return BadRequest(new { error = "User ID is required" }); + } + + if (string.IsNullOrWhiteSpace(webhook.Wallet?.Address)) + { + return BadRequest(new { error = "Wallet address is required" }); + } + + // Validate linked accounts + if (webhook.User.LinkedAccounts == null || !webhook.User.LinkedAccounts.Any()) + { + return BadRequest(new { error = "At least one linked account is required" }); + } + + if (webhook.User.LinkedAccounts.Count > 1) + { + _logger.LogWarning("Webhook received with {Count} linked accounts, expected 1. Using first account.", + webhook.User.LinkedAccounts.Count); + } + + // Get the first linked account (as per requirement) + var linkedAccount = webhook.User.LinkedAccounts.First(); + + // Determine ExternalEthereumAccount or TwitterAccount based on linked account type + string? externalEthereumAccount = null; + string? twitterAccount = null; + + var accountType = linkedAccount.Type?.ToLowerInvariant(); + switch (accountType) + { + case "ethereum": + case "wallet": + case "evm": + externalEthereumAccount = linkedAccount.Address; + break; + case "twitter": + case "twitter_oauth": + twitterAccount = linkedAccount.Address; + break; + default: + _logger.LogWarning("Unknown linked account type: {Type}, address: {Address}", + linkedAccount.Type, linkedAccount.Address); + // Could be email or other types - we'll just log it + break; + } + + // Process the webhook + var whitelistAccount = await _whitelistService.ProcessPrivyWebhookAsync( + webhook.User.Id, + webhook.User.CreatedAt, + webhook.Wallet.Address, + externalEthereumAccount, + twitterAccount); + + _logger.LogInformation("Privy webhook processed successfully - AccountId: {Id}, PrivyId: {PrivyId}", + whitelistAccount.Id, whitelistAccount.PrivyId); + + return Ok(whitelistAccount); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing Privy webhook"); + SentrySdk.CaptureException(ex); + return StatusCode(500, new { error = ex.Message }); + } + } + +} + +/// +/// Response model for paginated whitelist accounts. +/// +public class PaginatedWhitelistAccountsResponse +{ + public IEnumerable Accounts { get; set; } = new List(); + public int TotalCount { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } + public int TotalPages { get; set; } +} + + diff --git a/src/Managing.Api/Models/PrivyWebhookDto.cs b/src/Managing.Api/Models/PrivyWebhookDto.cs new file mode 100644 index 00000000..619c09ca --- /dev/null +++ b/src/Managing.Api/Models/PrivyWebhookDto.cs @@ -0,0 +1,79 @@ +using System.Text.Json.Serialization; + +namespace Managing.Api.Models; + +/// +/// Privy webhook payload for wallet creation events. +/// +public class PrivyWebhookDto +{ + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("user")] + public PrivyUserDto User { get; set; } = new(); + + [JsonPropertyName("wallet")] + public PrivyWalletDto Wallet { get; set; } = new(); +} + +/// +/// Privy user information from webhook. +/// +public class PrivyUserDto +{ + [JsonPropertyName("created_at")] + public long CreatedAt { get; set; } + + [JsonPropertyName("has_accepted_terms")] + public bool HasAcceptedTerms { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("is_guest")] + public bool IsGuest { get; set; } + + [JsonPropertyName("linked_accounts")] + public List LinkedAccounts { get; set; } = new(); + + [JsonPropertyName("mfa_methods")] + public List MfaMethods { get; set; } = new(); +} + +/// +/// Privy linked account information from webhook. +/// +public class PrivyLinkedAccountDto +{ + [JsonPropertyName("address")] + public string Address { get; set; } = string.Empty; + + [JsonPropertyName("first_verified_at")] + public long? FirstVerifiedAt { get; set; } + + [JsonPropertyName("latest_verified_at")] + public long? LatestVerifiedAt { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("verified_at")] + public long? VerifiedAt { get; set; } +} + +/// +/// Privy wallet information from webhook. +/// +public class PrivyWalletDto +{ + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("address")] + public string Address { get; set; } = string.Empty; + + [JsonPropertyName("chain_type")] + public string ChainType { get; set; } = string.Empty; +} + diff --git a/src/Managing.Api/Program.cs b/src/Managing.Api/Program.cs index f8589e26..6627cc92 100644 --- a/src/Managing.Api/Program.cs +++ b/src/Managing.Api/Program.cs @@ -14,6 +14,7 @@ using Managing.Infrastructure.Databases.InfluxDb.Models; using Managing.Infrastructure.Databases.PostgreSql; using Managing.Infrastructure.Databases.PostgreSql.Configurations; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -223,7 +224,8 @@ if (jwtSecret.Length < 32) // Get issuer and audience configuration var validIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"]; -var validAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"]; +var validAudiences = builder.Configuration.GetSection("Authentication:Schemes:Bearer:ValidAudiences") + .Get() ?? Array.Empty(); // Determine if validation should be enabled (enable in production, allow override via config) var enableIssuerValidation = builder.Configuration.GetValue("Jwt:ValidateIssuer", @@ -247,11 +249,11 @@ builder.Services o.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = validIssuer, - ValidAudience = validAudience, + ValidAudiences = validAudiences.Length > 0 ? validAudiences : null, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)), ValidateIssuerSigningKey = true, ValidateIssuer = enableIssuerValidation && !string.IsNullOrWhiteSpace(validIssuer), - ValidateAudience = enableAudienceValidation && !string.IsNullOrWhiteSpace(validAudience), + ValidateAudience = enableAudienceValidation && validAudiences.Length > 0, ValidateLifetime = true, // Explicitly validate token expiration ClockSkew = clockSkew, // Configure clock skew tolerance RequireExpirationTime = true, // Ensure tokens have expiration @@ -275,6 +277,19 @@ builder.Services var logger = context.HttpContext.RequestServices .GetService>(); + // Check if the endpoint allows anonymous access + var endpoint = context.HttpContext.GetEndpoint(); + var allowAnonymous = endpoint?.Metadata.GetMetadata() != null; + + // For anonymous endpoints with malformed tokens, skip authentication instead of failing + if (allowAnonymous && context.Exception is SecurityTokenMalformedException) + { + logger?.LogDebug("Skipping malformed token validation for anonymous endpoint: {Path}", + context.Request.Path); + context.NoResult(); // Skip authentication, don't fail + return Task.CompletedTask; + } + if (context.Exception is SecurityTokenExpiredException) { context.Response.Headers["Token-Expired"] = "true"; diff --git a/src/Managing.Api/appsettings.Production.json b/src/Managing.Api/appsettings.Production.json index fd179b3f..37a1b2e3 100644 --- a/src/Managing.Api/appsettings.Production.json +++ b/src/Managing.Api/appsettings.Production.json @@ -44,14 +44,18 @@ "Cors": { "AllowedOrigins": [ "https://app.kaigen.ai", - "https://api.kaigen.ai" + "https://api.kaigen.ai", + "https://web-ui.kai.managing.live" ] }, "Authentication": { "Schemes": { "Bearer": { "ValidIssuer": "https://api.kaigen.ai", - "ValidAudiences": "https://app.kaigen.ai" + "ValidAudiences": [ + "https://app.kaigen.ai", + "https://web-ui.kai.managing.live" + ] } } }, diff --git a/src/Managing.Application.Abstractions/Repositories/IWhitelistRepository.cs b/src/Managing.Application.Abstractions/Repositories/IWhitelistRepository.cs new file mode 100644 index 00000000..afafc0f9 --- /dev/null +++ b/src/Managing.Application.Abstractions/Repositories/IWhitelistRepository.cs @@ -0,0 +1,19 @@ +using Managing.Domain.Whitelist; + +namespace Managing.Application.Abstractions.Repositories; + +public interface IWhitelistRepository +{ + Task<(IEnumerable Accounts, int TotalCount)> GetPaginatedAsync( + int pageNumber, + int pageSize, + string? searchExternalEthereumAccount = null, + string? searchTwitterAccount = null); + + Task SetIsWhitelistedAsync(IEnumerable accountIds, bool isWhitelisted); + Task GetByIdAsync(int id); + Task GetByPrivyIdAsync(string privyId); + Task GetByEmbeddedWalletAsync(string embeddedWallet); + Task CreateOrUpdateAsync(WhitelistAccount whitelistAccount); +} + diff --git a/src/Managing.Application.Abstractions/Services/IWhitelistService.cs b/src/Managing.Application.Abstractions/Services/IWhitelistService.cs new file mode 100644 index 00000000..2a15e597 --- /dev/null +++ b/src/Managing.Application.Abstractions/Services/IWhitelistService.cs @@ -0,0 +1,17 @@ +using Managing.Domain.Whitelist; + +namespace Managing.Application.Abstractions.Services; + +public interface IWhitelistService +{ + Task<(IEnumerable Accounts, int TotalCount)> GetPaginatedAsync( + int pageNumber, + int pageSize, + string? searchExternalEthereumAccount = null, + string? searchTwitterAccount = null); + + Task SetIsWhitelistedAsync(IEnumerable accountIds, bool isWhitelisted); + Task GetByIdAsync(int id); + Task ProcessPrivyWebhookAsync(string privyUserId, long privyCreatedAt, string walletAddress, string? externalEthereumAccount, string? twitterAccount); +} + diff --git a/src/Managing.Application/Whitelist/WhitelistService.cs b/src/Managing.Application/Whitelist/WhitelistService.cs new file mode 100644 index 00000000..bf86f55d --- /dev/null +++ b/src/Managing.Application/Whitelist/WhitelistService.cs @@ -0,0 +1,111 @@ +using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Services; +using Managing.Domain.Whitelist; +using Microsoft.Extensions.Logging; + +namespace Managing.Application.Whitelist; + +public class WhitelistService : IWhitelistService +{ + private readonly IWhitelistRepository _whitelistRepository; + private readonly ILogger _logger; + + public WhitelistService( + IWhitelistRepository whitelistRepository, + ILogger logger) + { + _whitelistRepository = whitelistRepository; + _logger = logger; + } + + public async Task<(IEnumerable Accounts, int TotalCount)> GetPaginatedAsync( + int pageNumber, + int pageSize, + string? searchExternalEthereumAccount = null, + string? searchTwitterAccount = null) + { + if (pageNumber < 1) + { + throw new ArgumentException("Page number must be greater than 0", nameof(pageNumber)); + } + + if (pageSize < 1 || pageSize > 100) + { + throw new ArgumentException("Page size must be between 1 and 100", nameof(pageSize)); + } + + return await _whitelistRepository.GetPaginatedAsync( + pageNumber, + pageSize, + searchExternalEthereumAccount, + searchTwitterAccount); + } + + public async Task SetIsWhitelistedAsync(IEnumerable accountIds, bool isWhitelisted) + { + var idsList = accountIds?.ToList() ?? new List(); + + if (!idsList.Any()) + { + throw new ArgumentException("At least one account ID must be provided", nameof(accountIds)); + } + + _logger.LogInformation("Setting IsWhitelisted to {IsWhitelisted} for {Count} account(s)", + isWhitelisted, idsList.Count); + + var updatedCount = await _whitelistRepository.SetIsWhitelistedAsync(idsList, isWhitelisted); + + _logger.LogInformation("Successfully updated {Count} account(s) to IsWhitelisted = {IsWhitelisted}", + updatedCount, isWhitelisted); + + return updatedCount; + } + + public async Task GetByIdAsync(int id) + { + return await _whitelistRepository.GetByIdAsync(id); + } + + public async Task ProcessPrivyWebhookAsync( + string privyUserId, + long privyCreatedAt, + string walletAddress, + string? externalEthereumAccount, + string? twitterAccount) + { + _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; + + // Check if account already exists + var existing = await _whitelistRepository.GetByPrivyIdAsync(privyUserId) ?? + await _whitelistRepository.GetByEmbeddedWalletAsync(walletAddress); + + var whitelistAccount = new WhitelistAccount + { + PrivyId = privyUserId, + PrivyCreationDate = privyCreationDate, + EmbeddedWallet = walletAddress, + ExternalEthereumAccount = externalEthereumAccount, + TwitterAccount = twitterAccount, + IsWhitelisted = false, // Default to false, admin will set to true later + CreatedAt = existing?.CreatedAt ?? DateTime.UtcNow + }; + + // If existing, preserve the ID + if (existing != null) + { + whitelistAccount.Id = existing.Id; + } + + var result = await _whitelistRepository.CreateOrUpdateAsync(whitelistAccount); + + _logger.LogInformation("Privy webhook processed successfully - AccountId: {Id}, PrivyId: {PrivyId}", + result.Id, result.PrivyId); + + return result; + } +} + diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index c99ad7a1..fee3314b 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -23,6 +23,7 @@ using Managing.Application.Trading; using Managing.Application.Trading.Commands; using Managing.Application.Trading.Handlers; using Managing.Application.Users; +using Managing.Application.Whitelist; using Managing.Application.Workers; using Managing.Domain.Trades; using Managing.Infrastructure.Databases; @@ -408,6 +409,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); @@ -449,6 +451,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // InfluxDb Repositories - Use Singleton for proper connection management in Orleans grains services.AddSingleton(); diff --git a/src/Managing.Domain/Whitelist/WhitelistAccount.cs b/src/Managing.Domain/Whitelist/WhitelistAccount.cs new file mode 100644 index 00000000..8cd712f4 --- /dev/null +++ b/src/Managing.Domain/Whitelist/WhitelistAccount.cs @@ -0,0 +1,15 @@ +namespace Managing.Domain.Whitelist; + +public class WhitelistAccount +{ + public int Id { get; set; } + public required string PrivyId { get; set; } + public DateTime PrivyCreationDate { get; set; } + public required string EmbeddedWallet { get; set; } + public string? ExternalEthereumAccount { get; set; } + public string? TwitterAccount { get; set; } + public bool IsWhitelisted { get; set; } = false; + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + diff --git a/src/Managing.Infrastructure.Database/Migrations/20251107122139_AddWhitelistAccountsTable.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251107122139_AddWhitelistAccountsTable.Designer.cs new file mode 100644 index 00000000..20faa7c0 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251107122139_AddWhitelistAccountsTable.Designer.cs @@ -0,0 +1,1602 @@ +// +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("20251107122139_AddWhitelistAccountsTable")] + partial class AddWhitelistAccountsTable + { + /// + 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("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/20251107122139_AddWhitelistAccountsTable.cs b/src/Managing.Infrastructure.Database/Migrations/20251107122139_AddWhitelistAccountsTable.cs new file mode 100644 index 00000000..6f8fd4f0 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251107122139_AddWhitelistAccountsTable.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class AddWhitelistAccountsTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "WhitelistAccounts", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PrivyId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + PrivyCreationDate = table.Column(type: "timestamp with time zone", nullable: false), + EmbeddedWallet = table.Column(type: "character varying(42)", maxLength: 42, nullable: false), + ExternalEthereumAccount = table.Column(type: "character varying(42)", maxLength: 42, nullable: true), + TwitterAccount = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + IsWhitelisted = table.Column(type: "boolean", nullable: false, defaultValue: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WhitelistAccounts", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_WhitelistAccounts_CreatedAt", + table: "WhitelistAccounts", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_WhitelistAccounts_EmbeddedWallet", + table: "WhitelistAccounts", + column: "EmbeddedWallet", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WhitelistAccounts_ExternalEthereumAccount", + table: "WhitelistAccounts", + column: "ExternalEthereumAccount"); + + migrationBuilder.CreateIndex( + name: "IX_WhitelistAccounts_PrivyId", + table: "WhitelistAccounts", + column: "PrivyId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WhitelistAccounts_TwitterAccount", + table: "WhitelistAccounts", + column: "TwitterAccount"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "WhitelistAccounts"); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index c1b07d5a..504326ca 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -1325,6 +1325,63 @@ namespace Managing.Infrastructure.Databases.Migrations 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") diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/WhitelistAccountEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/WhitelistAccountEntity.cs new file mode 100644 index 00000000..32d9942f --- /dev/null +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/WhitelistAccountEntity.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Managing.Infrastructure.Databases.PostgreSql.Entities; + +[Table("WhitelistAccounts")] +public class WhitelistAccountEntity +{ + [Key] public int Id { get; set; } + + [Required] + [MaxLength(255)] + public required string PrivyId { get; set; } + + [Required] + public DateTime PrivyCreationDate { get; set; } + + [Required] + [MaxLength(42)] + public required string EmbeddedWallet { get; set; } + + [MaxLength(42)] + public string? ExternalEthereumAccount { get; set; } + + [MaxLength(255)] + public string? TwitterAccount { get; set; } + + [Required] + public bool IsWhitelisted { get; set; } = false; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime? UpdatedAt { get; set; } +} + diff --git a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs index ee1ddaa5..438ac8cd 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs @@ -55,6 +55,7 @@ public class ManagingDbContext : DbContext public DbSet SynthMinersLeaderboards { get; set; } public DbSet SynthPredictions { get; set; } + public DbSet WhitelistAccounts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -573,6 +574,29 @@ public class ManagingDbContext : DbContext entity.HasIndex(e => e.CacheKey).IsUnique(); }); + // Configure WhitelistAccount entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.PrivyId).IsRequired().HasMaxLength(255); + entity.Property(e => e.PrivyCreationDate).IsRequired(); + entity.Property(e => e.EmbeddedWallet).IsRequired().HasMaxLength(42); + entity.Property(e => e.ExternalEthereumAccount).HasMaxLength(42); + entity.Property(e => e.TwitterAccount).HasMaxLength(255); + entity.Property(e => e.IsWhitelisted) + .IsRequired() + .HasDefaultValue(false); + entity.Property(e => e.CreatedAt).IsRequired(); + entity.Property(e => e.UpdatedAt); + + // Create indexes for search performance + entity.HasIndex(e => e.PrivyId).IsUnique(); + entity.HasIndex(e => e.EmbeddedWallet).IsUnique(); + entity.HasIndex(e => e.ExternalEthereumAccount); + entity.HasIndex(e => e.TwitterAccount); + entity.HasIndex(e => e.CreatedAt); + }); + // Configure AgentSummary entity modelBuilder.Entity(entity => { diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index 931b164b..eb141a01 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -10,6 +10,7 @@ using Managing.Domain.Statistics; using Managing.Domain.Strategies; using Managing.Domain.Trades; using Managing.Domain.Users; +using Managing.Domain.Whitelist; using Managing.Domain.Workers; using Managing.Infrastructure.Databases.PostgreSql.Entities; using Newtonsoft.Json; @@ -992,6 +993,51 @@ public static class PostgreSqlMappers #endregion + #region WhitelistAccount Mappings + + public static WhitelistAccount Map(WhitelistAccountEntity entity) + { + if (entity == null) return null; + + return new WhitelistAccount + { + Id = entity.Id, + PrivyId = entity.PrivyId, + PrivyCreationDate = entity.PrivyCreationDate, + EmbeddedWallet = entity.EmbeddedWallet, + ExternalEthereumAccount = entity.ExternalEthereumAccount, + TwitterAccount = entity.TwitterAccount, + IsWhitelisted = entity.IsWhitelisted, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt + }; + } + + public static WhitelistAccountEntity Map(WhitelistAccount whitelistAccount) + { + if (whitelistAccount == null) return null; + + return new WhitelistAccountEntity + { + Id = whitelistAccount.Id, + PrivyId = whitelistAccount.PrivyId, + PrivyCreationDate = whitelistAccount.PrivyCreationDate, + EmbeddedWallet = whitelistAccount.EmbeddedWallet, + ExternalEthereumAccount = whitelistAccount.ExternalEthereumAccount, + TwitterAccount = whitelistAccount.TwitterAccount, + IsWhitelisted = whitelistAccount.IsWhitelisted, + CreatedAt = whitelistAccount.CreatedAt, + UpdatedAt = whitelistAccount.UpdatedAt + }; + } + + public static IEnumerable Map(IEnumerable entities) + { + return entities?.Select(Map) ?? Enumerable.Empty(); + } + + #endregion + private static int? ExtractBundleIndex(string name) { if (string.IsNullOrWhiteSpace(name)) return null; diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlWhitelistRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlWhitelistRepository.cs new file mode 100644 index 00000000..550ee7ff --- /dev/null +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlWhitelistRepository.cs @@ -0,0 +1,237 @@ +using Managing.Application.Abstractions.Repositories; +using Managing.Domain.Whitelist; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Managing.Infrastructure.Databases.PostgreSql; + +public class PostgreSqlWhitelistRepository : BaseRepositoryWithLogging, IWhitelistRepository +{ + public PostgreSqlWhitelistRepository( + ManagingDbContext context, + ILogger logger, + SentrySqlMonitoringService sentryMonitoringService) + : base(context, logger, sentryMonitoringService) + { + } + + public async Task<(IEnumerable Accounts, int TotalCount)> GetPaginatedAsync( + int pageNumber, + int pageSize, + string? searchExternalEthereumAccount = null, + string? searchTwitterAccount = null) + { + return await ExecuteWithLoggingAsync(async () => + { + try + { + await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); + + var query = _context.WhitelistAccounts.AsNoTracking().AsQueryable(); + + // Apply search filters + if (!string.IsNullOrWhiteSpace(searchExternalEthereumAccount)) + { + var searchTerm = searchExternalEthereumAccount.Trim().ToLower(); + query = query.Where(e => + e.ExternalEthereumAccount != null && + e.ExternalEthereumAccount.ToLower().Contains(searchTerm)); + } + + if (!string.IsNullOrWhiteSpace(searchTwitterAccount)) + { + var searchTerm = searchTwitterAccount.Trim().ToLower(); + query = query.Where(e => + e.TwitterAccount != null && + e.TwitterAccount.ToLower().Contains(searchTerm)); + } + + // Get total count before pagination + var totalCount = await query.CountAsync().ConfigureAwait(false); + + // Apply pagination and ordering + var entities = await query + .OrderByDescending(e => e.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync() + .ConfigureAwait(false); + + var accounts = entities.Select(PostgreSqlMappers.Map); + + return (accounts, totalCount); + } + finally + { + await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); + } + }, nameof(GetPaginatedAsync), + ("pageNumber", pageNumber.ToString()), + ("pageSize", pageSize.ToString())); + } + + public async Task SetIsWhitelistedAsync(IEnumerable accountIds, bool isWhitelisted) + { + return await ExecuteWithLoggingAsync(async () => + { + try + { + await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); + + var idsList = accountIds.ToList(); + if (!idsList.Any()) + { + return 0; + } + + var entities = await _context.WhitelistAccounts + .AsTracking() + .Where(e => idsList.Contains(e.Id)) + .ToListAsync() + .ConfigureAwait(false); + + foreach (var entity in entities) + { + entity.IsWhitelisted = isWhitelisted; + entity.UpdatedAt = DateTime.UtcNow; + } + + var updatedCount = await _context.SaveChangesAsync().ConfigureAwait(false); + + return updatedCount; + } + finally + { + await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); + } + }, nameof(SetIsWhitelistedAsync), + ("accountIds", string.Join(",", accountIds)), + ("isWhitelisted", isWhitelisted.ToString())); + } + + public async Task GetByIdAsync(int id) + { + return await ExecuteWithLoggingAsync(async () => + { + try + { + await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); + + var entity = await _context.WhitelistAccounts + .AsNoTracking() + .FirstOrDefaultAsync(e => e.Id == id) + .ConfigureAwait(false); + + return entity != null ? PostgreSqlMappers.Map(entity) : null; + } + finally + { + await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); + } + }, nameof(GetByIdAsync), ("id", id.ToString())); + } + + public async Task GetByPrivyIdAsync(string privyId) + { + return await ExecuteWithLoggingAsync(async () => + { + try + { + await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); + + var entity = await _context.WhitelistAccounts + .AsNoTracking() + .FirstOrDefaultAsync(e => e.PrivyId == privyId) + .ConfigureAwait(false); + + return entity != null ? PostgreSqlMappers.Map(entity) : null; + } + finally + { + await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); + } + }, nameof(GetByPrivyIdAsync), ("privyId", privyId)); + } + + public async Task GetByEmbeddedWalletAsync(string embeddedWallet) + { + return await ExecuteWithLoggingAsync(async () => + { + try + { + await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); + + var entity = await _context.WhitelistAccounts + .AsNoTracking() + .FirstOrDefaultAsync(e => e.EmbeddedWallet.ToLower() == embeddedWallet.ToLower()) + .ConfigureAwait(false); + + return entity != null ? PostgreSqlMappers.Map(entity) : null; + } + finally + { + await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); + } + }, nameof(GetByEmbeddedWalletAsync), ("embeddedWallet", embeddedWallet)); + } + + public async Task CreateOrUpdateAsync(WhitelistAccount whitelistAccount) + { + return await ExecuteWithLoggingAsync(async () => + { + try + { + await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); + + // Check if account exists by PrivyId or EmbeddedWallet + var existing = await _context.WhitelistAccounts + .AsTracking() + .FirstOrDefaultAsync(e => + e.PrivyId == whitelistAccount.PrivyId || + e.EmbeddedWallet.ToLower() == whitelistAccount.EmbeddedWallet.ToLower()) + .ConfigureAwait(false); + + if (existing != null) + { + // Update existing account + existing.PrivyId = whitelistAccount.PrivyId; + existing.PrivyCreationDate = whitelistAccount.PrivyCreationDate; + existing.EmbeddedWallet = whitelistAccount.EmbeddedWallet; + + // Only update if new value is provided + if (!string.IsNullOrWhiteSpace(whitelistAccount.ExternalEthereumAccount)) + { + existing.ExternalEthereumAccount = whitelistAccount.ExternalEthereumAccount; + } + + if (!string.IsNullOrWhiteSpace(whitelistAccount.TwitterAccount)) + { + existing.TwitterAccount = whitelistAccount.TwitterAccount; + } + + existing.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync().ConfigureAwait(false); + + return PostgreSqlMappers.Map(existing); + } + else + { + // Create new account + var entity = PostgreSqlMappers.Map(whitelistAccount); + entity.CreatedAt = DateTime.UtcNow; + + _context.WhitelistAccounts.Add(entity); + await _context.SaveChangesAsync().ConfigureAwait(false); + + return PostgreSqlMappers.Map(entity); + } + } + finally + { + await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); + } + }, nameof(CreateOrUpdateAsync), ("privyId", whitelistAccount.PrivyId)); + } +} +