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 IWebhookService _webhookService; private readonly ILogger _logger; private const string AlertsChannel = "2676086723"; public WhitelistService( IWhitelistRepository whitelistRepository, IWebhookService webhookService, ILogger logger) { _whitelistRepository = whitelistRepository; _webhookService = webhookService; _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); // Send notification to Alert channel when users are whitelisted if (isWhitelisted && updatedCount > 0) { try { await SendWhitelistNotificationAsync(idsList); } catch (Exception ex) { _logger.LogError(ex, "Failed to send whitelist notification for account IDs: {AccountIds}", string.Join(", ", idsList)); SentrySdk.CaptureException(ex); // Don't throw - notification failure shouldn't fail the whitelist operation } } return updatedCount; } private async Task SendWhitelistNotificationAsync(List accountIds) { var accounts = new List(); // Fetch account details for the notification foreach (var id in accountIds) { var account = await _whitelistRepository.GetByIdAsync(id); if (account != null) { accounts.Add(account); } } if (!accounts.Any()) { _logger.LogWarning("No accounts found to send whitelist notification for IDs: {AccountIds}", string.Join(", ", accountIds)); return; } // Build notification message var message = accounts.Count == 1 ? BuildSingleAccountWhitelistMessage(accounts[0]) : BuildMultipleAccountsWhitelistMessage(accounts); await _webhookService.SendMessage(message, AlertsChannel); _logger.LogInformation("Sent whitelist notification to Alert channel for {Count} account(s)", accounts.Count); } private string BuildSingleAccountWhitelistMessage(WhitelistAccount account) { var message = $"✅ **User Whitelisted**\n" + $"Account ID: `{account.Id}`\n" + $"Privy ID: `{account.PrivyId}`\n" + $"Embedded Wallet: `{account.EmbeddedWallet}`\n"; if (!string.IsNullOrWhiteSpace(account.ExternalEthereumAccount)) { message += $"External Ethereum: `{account.ExternalEthereumAccount}`\n"; } if (!string.IsNullOrWhiteSpace(account.TwitterAccount)) { message += $"Twitter: `{account.TwitterAccount}`\n"; } message += $"Time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; return message; } private string BuildMultipleAccountsWhitelistMessage(List accounts) { var message = $"✅ **{accounts.Count} Users Whitelisted**\n\n"; foreach (var account in accounts) { message += $"• Account ID: `{account.Id}` | Privy ID: `{account.PrivyId}`\n"; message += $" Wallet: `{account.EmbeddedWallet}`\n"; if (!string.IsNullOrWhiteSpace(account.ExternalEthereumAccount)) { message += $" External Ethereum: `{account.ExternalEthereumAccount}`\n"; } if (!string.IsNullOrWhiteSpace(account.TwitterAccount)) { message += $" Twitter: `{account.TwitterAccount}`\n"; } message += "\n"; } message += $"Time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; return message; } public async Task GetByIdAsync(int id) { 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, 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 UTC DateTime (PostgreSQL requires UTC) var privyCreationDate = DateTimeOffset.FromUnixTimeSeconds(privyCreatedAt).UtcDateTime; // 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; } }