From b3f3df5fbc107eba63094bac7dbe2a62ebfb71e3 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 10 Nov 2025 17:57:00 +0700 Subject: [PATCH] Fix privy secrets --- .../Whitelist/WhitelistService.cs | 116 ++++++++++++++++-- src/Managing.Bootstrap/ApiBootstrap.cs | 2 +- .../src/plugins/custom/privy-secrets.ts | 60 ++++++++- .../src/plugins/custom/privy.ts | 37 ++++-- 4 files changed, 192 insertions(+), 23 deletions(-) diff --git a/src/Managing.Application/Whitelist/WhitelistService.cs b/src/Managing.Application/Whitelist/WhitelistService.cs index 1ba8eab3..b18dea02 100644 --- a/src/Managing.Application/Whitelist/WhitelistService.cs +++ b/src/Managing.Application/Whitelist/WhitelistService.cs @@ -8,13 +8,17 @@ 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; } @@ -44,23 +48,119 @@ public class WhitelistService : IWhitelistService 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)", + _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}", + _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); @@ -74,7 +174,7 @@ public class WhitelistService : IWhitelistService } var account = await _whitelistRepository.GetByEmbeddedWalletAsync(embeddedWallet); - + return account?.IsWhitelisted ?? false; } @@ -85,7 +185,8 @@ public class WhitelistService : IWhitelistService string? externalEthereumAccount, string? twitterAccount) { - _logger.LogInformation("Processing Privy webhook - PrivyId: {PrivyId}, Wallet: {Wallet}, ExternalEthereum: {ExternalEthereum}, Twitter: {Twitter}", + _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) @@ -93,7 +194,7 @@ public class WhitelistService : IWhitelistService // Check if account already exists var existing = await _whitelistRepository.GetByPrivyIdAsync(privyUserId) ?? - await _whitelistRepository.GetByEmbeddedWalletAsync(walletAddress); + await _whitelistRepository.GetByEmbeddedWalletAsync(walletAddress); var whitelistAccount = new WhitelistAccount { @@ -119,5 +220,4 @@ public class WhitelistService : IWhitelistService return result; } -} - +} \ No newline at end of file diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index 4deb48fb..5bda770d 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -408,7 +408,7 @@ public static class ApiBootstrap services.AddTransient(); - services.AddTransient(); + services.AddHttpClient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts b/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts index a5beeef3..9ac14e27 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts @@ -12,6 +12,28 @@ declare module 'fastify' { } } +// Module-level cache for Privy secrets - accessible without Fastify instance +// This is populated ONCE at startup and never changed during runtime +let cachedPrivySecrets: { + appId: string + appSecret: string + authKey: string +} | null = null + +/** + * Sets the cached Privy secrets (called once at startup) + */ +export function setCachedPrivySecrets(secrets: { appId: string; appSecret: string; authKey: string }) { + cachedPrivySecrets = secrets +} + +/** + * Gets the cached Privy secrets (for use without Fastify instance) + */ +export function getCachedPrivySecrets(): { appId: string; appSecret: string; authKey: string } | null { + return cachedPrivySecrets +} + /** * Loads Privy secrets ONCE at application startup from Infisical (production) or env vars/files (non-production). * Secrets are cached in memory via Fastify decorator and never reloaded during runtime. @@ -81,10 +103,32 @@ export default fp(async function (fastify) { const privyAppSecretSecret = allSecrets.secrets?.find(s => s.secretKey === 'PRIVY_APP_SECRET') const privyAuthKeySecret = allSecrets.secrets?.find(s => s.secretKey === 'PRIVY_AUTHORIZATION_KEY') + // Log which secrets were found + fastify.log.info({ + foundAppId: !!privyAppIdSecret, + foundAppSecret: !!privyAppSecretSecret, + foundAuthKey: !!privyAuthKeySecret, + allSecretKeys: allSecrets.secrets?.map(s => s.secretKey) || [] + }, 'Checking for Privy secrets in Infisical response') + appId = privyAppIdSecret?.secretValue || process.env.PRIVY_APP_ID || '' appSecret = privyAppSecretSecret?.secretValue || process.env.PRIVY_APP_SECRET || '' authKey = privyAuthKeySecret?.secretValue || process.env.PRIVY_AUTHORIZATION_KEY || '' + // Validate that we got the secrets + if (!appId || !appSecret || !authKey) { + fastify.log.error({ + hasAppId: !!appId, + hasAppSecret: !!appSecret, + hasAuthKey: !!authKey, + fromInfisical: { + appId: !!privyAppIdSecret, + appSecret: !!privyAppSecretSecret, + authKey: !!privyAuthKeySecret + } + }, '⚠️ Privy secrets incomplete after loading from Infisical') + } + fastify.log.info({ appId: !!appId, appSecret: !!appSecret, @@ -139,6 +183,13 @@ export default fp(async function (fastify) { nodeEnv: process.env.NODE_ENV }, '⚠️ WARNING: Privy secrets not fully resolved at plugin load - app will continue but Privy operations will fail') + // Store in module-level cache (even if empty, so getPrivyClient can detect the issue) + setCachedPrivySecrets({ + appId: appId || '', + appSecret: appSecret || '', + authKey: authKey || '' + }) + // Still decorate with empty strings so the app doesn't crash // The actual error will be thrown in getPrivyClient when it's used fastify.decorate('privySecrets', { @@ -164,6 +215,13 @@ export default fp(async function (fastify) { return // Continue without throwing } + // Store secrets in module-level cache (accessible without Fastify instance) + setCachedPrivySecrets({ + appId, + appSecret, + authKey + }) + // Decorate Fastify instance with secrets - these are cached in memory for the lifetime of the application // This decoration happens ONCE at startup, and secrets are reused from memory for all Privy client creations fastify.decorate('privySecrets', { @@ -176,7 +234,7 @@ export default fp(async function (fastify) { appId: appId.substring(0, 10) + '...', appSecret: appSecret.substring(0, 10) + '...', authKey: authKey.substring(0, 20) + '...', - note: 'Secrets cached in memory - Infisical will not be called again during runtime' + note: 'Secrets cached in memory (module-level and Fastify decorator) - Infisical will not be called again during runtime' }, '✅ Privy secrets loaded at startup and cached in memory') }, { name: 'privy-secrets', diff --git a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts index f88f2e89..38784d59 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts @@ -14,6 +14,7 @@ import {CONTRACTS} from '../../generated/gmxsdk/configs/contracts.js' import {getClientForAddress, getTokenDataFromTicker} from './gmx.js' import {Address} from 'viem' import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js' +import {getCachedPrivySecrets} from './privy-secrets.js' // Load environment variables in non-production only if (process.env.NODE_ENV !== 'production') dotenv.config() @@ -59,16 +60,20 @@ declare module 'fastify' { */ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { // Read from cached secrets in memory (loaded once at startup): - // 1. Fastify decorator (populated by privy-secrets plugin at startup - Infisical in prod, env in dev) - // 2. Fastify config (from env plugin) - // 3. Environment variables (fallback) - // Note: In production, secrets come from Infisical and are cached in fastify.privySecrets at startup + // Priority order: + // 1. Module-level cache (populated by privy-secrets plugin at startup - accessible without Fastify instance) + // 2. Fastify decorator (populated by privy-secrets plugin at startup - Infisical in prod, env in dev) + // 3. Fastify config (from env plugin) + // 4. Environment variables (fallback) + // Note: In production, secrets come from Infisical and are cached in module-level variable and fastify.privySecrets at startup + // First, try module-level cache (works without Fastify instance) + const cachedSecrets = getCachedPrivySecrets() const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string; authKey: string } | undefined - const appId = decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID - const appSecret = decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET - const authKey = decorated?.authKey || fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY + const appId = cachedSecrets?.appId || decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID + const appSecret = cachedSecrets?.appSecret || decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET + const authKey = cachedSecrets?.authKey || decorated?.authKey || fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY if (!appId || !appSecret || !authKey) { console.warn('Missing Privy environment variables:'); @@ -76,6 +81,7 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { console.warn('PRIVY_APP_SECRET:', appSecret ? 'present' : 'missing'); console.warn('PRIVY_AUTHORIZATION_KEY:', authKey ? 'present' : 'missing'); console.warn('NODE_ENV:', process.env.NODE_ENV); + console.warn('Module-level cache available:', !!cachedSecrets); console.warn('Fastify privySecrets decorated:', !!decorated); console.warn('Fastify config available:', !!fastify?.config); @@ -108,8 +114,10 @@ export async function getAuthorizationSignature( fastify?: FastifyInstance ) { // Get app ID from cached secrets (loaded once at startup) or env fallback + // Priority: module-level cache > Fastify decorator > Fastify config > env vars + const cachedSecrets = getCachedPrivySecrets() const decorated = (fastify as any)?.privySecrets as { appId: string; authKey: string } | undefined - const appId = decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID || '' + const appId = cachedSecrets?.appId || decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID || '' const payload = { version: 1, @@ -127,10 +135,12 @@ export async function getAuthorizationSignature( const serializedPayloadBuffer = Buffer.from(serializedPayload); // Resolve authorization key from cached secrets (loaded once at startup from Infisical in production) + // Priority: module-level cache > Fastify decorator > Fastify config > file > env vars let resolvedKey: string | undefined - // Try Fastify decorator first (secrets cached in memory from Infisical at startup in production) - if (decorated?.authKey) { + if (cachedSecrets?.authKey) { + resolvedKey = cachedSecrets.authKey + } else if (decorated?.authKey) { resolvedKey = decorated.authKey } else if (fastify?.config?.PRIVY_AUTHORIZATION_KEY) { resolvedKey = fastify.config.PRIVY_AUTHORIZATION_KEY @@ -176,11 +186,12 @@ export const makePrivyRequest = async ( ): Promise => { try { // Resolve secrets from cached memory (loaded once at startup from Infisical in production) + // Priority: module-level cache > Fastify decorator > Fastify config > env vars + const cachedSecrets = getCachedPrivySecrets() const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string } | undefined - // Try Fastify decorator first (secrets cached in memory from Infisical at startup in production) - const appId = decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID || '' - const appSecret = decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET || '' + const appId = cachedSecrets?.appId || decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID || '' + const appSecret = cachedSecrets?.appSecret || decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET || '' let headers: Record = { 'Content-Type': 'application/json',