Fix privy secrets

This commit is contained in:
2025-11-10 17:57:00 +07:00
parent fec1c78b3c
commit b3f3df5fbc
4 changed files with 192 additions and 23 deletions

View File

@@ -8,13 +8,17 @@ namespace Managing.Application.Whitelist;
public class WhitelistService : IWhitelistService public class WhitelistService : IWhitelistService
{ {
private readonly IWhitelistRepository _whitelistRepository; private readonly IWhitelistRepository _whitelistRepository;
private readonly IWebhookService _webhookService;
private readonly ILogger<WhitelistService> _logger; private readonly ILogger<WhitelistService> _logger;
private const string AlertsChannel = "2676086723";
public WhitelistService( public WhitelistService(
IWhitelistRepository whitelistRepository, IWhitelistRepository whitelistRepository,
IWebhookService webhookService,
ILogger<WhitelistService> logger) ILogger<WhitelistService> logger)
{ {
_whitelistRepository = whitelistRepository; _whitelistRepository = whitelistRepository;
_webhookService = webhookService;
_logger = logger; _logger = logger;
} }
@@ -58,9 +62,105 @@ public class WhitelistService : IWhitelistService
_logger.LogInformation("Successfully updated {Count} account(s) to IsWhitelisted = {IsWhitelisted}", _logger.LogInformation("Successfully updated {Count} account(s) to IsWhitelisted = {IsWhitelisted}",
updatedCount, 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; return updatedCount;
} }
private async Task SendWhitelistNotificationAsync(List<int> accountIds)
{
var accounts = new List<WhitelistAccount>();
// 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<WhitelistAccount> 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<WhitelistAccount?> GetByIdAsync(int id) public async Task<WhitelistAccount?> GetByIdAsync(int id)
{ {
return await _whitelistRepository.GetByIdAsync(id); return await _whitelistRepository.GetByIdAsync(id);
@@ -85,7 +185,8 @@ public class WhitelistService : IWhitelistService
string? externalEthereumAccount, string? externalEthereumAccount,
string? twitterAccount) 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"); privyUserId, walletAddress, externalEthereumAccount ?? "null", twitterAccount ?? "null");
// Convert Unix timestamp to UTC DateTime (PostgreSQL requires UTC) // Convert Unix timestamp to UTC DateTime (PostgreSQL requires UTC)
@@ -93,7 +194,7 @@ public class WhitelistService : IWhitelistService
// Check if account already exists // Check if account already exists
var existing = await _whitelistRepository.GetByPrivyIdAsync(privyUserId) ?? var existing = await _whitelistRepository.GetByPrivyIdAsync(privyUserId) ??
await _whitelistRepository.GetByEmbeddedWalletAsync(walletAddress); await _whitelistRepository.GetByEmbeddedWalletAsync(walletAddress);
var whitelistAccount = new WhitelistAccount var whitelistAccount = new WhitelistAccount
{ {
@@ -120,4 +221,3 @@ public class WhitelistService : IWhitelistService
return result; return result;
} }
} }

View File

@@ -408,7 +408,7 @@ public static class ApiBootstrap
services.AddTransient<IWeb3ProxyService, Web3ProxyService>(); services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
services.AddTransient<IWebhookService, WebhookService>(); services.AddHttpClient<IWebhookService, WebhookService>();
services.AddTransient<IKaigenService, KaigenService>(); services.AddTransient<IKaigenService, KaigenService>();
services.AddTransient<IWhitelistService, WhitelistService>(); services.AddTransient<IWhitelistService, WhitelistService>();

View File

@@ -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). * 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. * 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 privyAppSecretSecret = allSecrets.secrets?.find(s => s.secretKey === 'PRIVY_APP_SECRET')
const privyAuthKeySecret = allSecrets.secrets?.find(s => s.secretKey === 'PRIVY_AUTHORIZATION_KEY') 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 || '' appId = privyAppIdSecret?.secretValue || process.env.PRIVY_APP_ID || ''
appSecret = privyAppSecretSecret?.secretValue || process.env.PRIVY_APP_SECRET || '' appSecret = privyAppSecretSecret?.secretValue || process.env.PRIVY_APP_SECRET || ''
authKey = privyAuthKeySecret?.secretValue || process.env.PRIVY_AUTHORIZATION_KEY || '' 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({ fastify.log.info({
appId: !!appId, appId: !!appId,
appSecret: !!appSecret, appSecret: !!appSecret,
@@ -139,6 +183,13 @@ export default fp(async function (fastify) {
nodeEnv: process.env.NODE_ENV nodeEnv: process.env.NODE_ENV
}, '⚠️ WARNING: Privy secrets not fully resolved at plugin load - app will continue but Privy operations will fail') }, '⚠️ 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 // Still decorate with empty strings so the app doesn't crash
// The actual error will be thrown in getPrivyClient when it's used // The actual error will be thrown in getPrivyClient when it's used
fastify.decorate('privySecrets', { fastify.decorate('privySecrets', {
@@ -164,6 +215,13 @@ export default fp(async function (fastify) {
return // Continue without throwing 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 // 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 // This decoration happens ONCE at startup, and secrets are reused from memory for all Privy client creations
fastify.decorate('privySecrets', { fastify.decorate('privySecrets', {
@@ -176,7 +234,7 @@ export default fp(async function (fastify) {
appId: appId.substring(0, 10) + '...', appId: appId.substring(0, 10) + '...',
appSecret: appSecret.substring(0, 10) + '...', appSecret: appSecret.substring(0, 10) + '...',
authKey: authKey.substring(0, 20) + '...', 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') }, '✅ Privy secrets loaded at startup and cached in memory')
}, { }, {
name: 'privy-secrets', name: 'privy-secrets',

View File

@@ -14,6 +14,7 @@ import {CONTRACTS} from '../../generated/gmxsdk/configs/contracts.js'
import {getClientForAddress, getTokenDataFromTicker} from './gmx.js' import {getClientForAddress, getTokenDataFromTicker} from './gmx.js'
import {Address} from 'viem' import {Address} from 'viem'
import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js' import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js'
import {getCachedPrivySecrets} from './privy-secrets.js'
// Load environment variables in non-production only // Load environment variables in non-production only
if (process.env.NODE_ENV !== 'production') dotenv.config() if (process.env.NODE_ENV !== 'production') dotenv.config()
@@ -59,16 +60,20 @@ declare module 'fastify' {
*/ */
export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
// Read from cached secrets in memory (loaded once at startup): // 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) // Priority order:
// 2. Fastify config (from env plugin) // 1. Module-level cache (populated by privy-secrets plugin at startup - accessible without Fastify instance)
// 3. Environment variables (fallback) // 2. Fastify decorator (populated by privy-secrets plugin at startup - Infisical in prod, env in dev)
// Note: In production, secrets come from Infisical and are cached in fastify.privySecrets at startup // 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 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 appId = cachedSecrets?.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 appSecret = cachedSecrets?.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 authKey = cachedSecrets?.authKey || decorated?.authKey || fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY
if (!appId || !appSecret || !authKey) { if (!appId || !appSecret || !authKey) {
console.warn('Missing Privy environment variables:'); 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_APP_SECRET:', appSecret ? 'present' : 'missing');
console.warn('PRIVY_AUTHORIZATION_KEY:', authKey ? 'present' : 'missing'); console.warn('PRIVY_AUTHORIZATION_KEY:', authKey ? 'present' : 'missing');
console.warn('NODE_ENV:', process.env.NODE_ENV); console.warn('NODE_ENV:', process.env.NODE_ENV);
console.warn('Module-level cache available:', !!cachedSecrets);
console.warn('Fastify privySecrets decorated:', !!decorated); console.warn('Fastify privySecrets decorated:', !!decorated);
console.warn('Fastify config available:', !!fastify?.config); console.warn('Fastify config available:', !!fastify?.config);
@@ -108,8 +114,10 @@ export async function getAuthorizationSignature(
fastify?: FastifyInstance fastify?: FastifyInstance
) { ) {
// Get app ID from cached secrets (loaded once at startup) or env fallback // 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 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 = { const payload = {
version: 1, version: 1,
@@ -127,10 +135,12 @@ export async function getAuthorizationSignature(
const serializedPayloadBuffer = Buffer.from(serializedPayload); const serializedPayloadBuffer = Buffer.from(serializedPayload);
// Resolve authorization key from cached secrets (loaded once at startup from Infisical in production) // 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 let resolvedKey: string | undefined
// Try Fastify decorator first (secrets cached in memory from Infisical at startup in production) if (cachedSecrets?.authKey) {
if (decorated?.authKey) { resolvedKey = cachedSecrets.authKey
} else if (decorated?.authKey) {
resolvedKey = decorated.authKey resolvedKey = decorated.authKey
} else if (fastify?.config?.PRIVY_AUTHORIZATION_KEY) { } else if (fastify?.config?.PRIVY_AUTHORIZATION_KEY) {
resolvedKey = fastify.config.PRIVY_AUTHORIZATION_KEY resolvedKey = fastify.config.PRIVY_AUTHORIZATION_KEY
@@ -176,11 +186,12 @@ export const makePrivyRequest = async <T>(
): Promise<T> => { ): Promise<T> => {
try { try {
// Resolve secrets from cached memory (loaded once at startup from Infisical in production) // 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 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 = cachedSecrets?.appId || decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID || ''
const 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 appSecret = decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET || ''
let headers: Record<string, string> = { let headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',