Fix privy secrets
This commit is contained in:
@@ -8,13 +8,17 @@ namespace Managing.Application.Whitelist;
|
||||
public class WhitelistService : IWhitelistService
|
||||
{
|
||||
private readonly IWhitelistRepository _whitelistRepository;
|
||||
private readonly IWebhookService _webhookService;
|
||||
private readonly ILogger<WhitelistService> _logger;
|
||||
private const string AlertsChannel = "2676086723";
|
||||
|
||||
public WhitelistService(
|
||||
IWhitelistRepository whitelistRepository,
|
||||
IWebhookService webhookService,
|
||||
ILogger<WhitelistService> logger)
|
||||
{
|
||||
_whitelistRepository = whitelistRepository;
|
||||
_webhookService = webhookService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -58,9 +62,105 @@ public class WhitelistService : IWhitelistService
|
||||
_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<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)
|
||||
{
|
||||
return await _whitelistRepository.GetByIdAsync(id);
|
||||
@@ -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
|
||||
{
|
||||
@@ -120,4 +221,3 @@ public class WhitelistService : IWhitelistService
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -408,7 +408,7 @@ public static class ApiBootstrap
|
||||
|
||||
|
||||
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
|
||||
services.AddTransient<IWebhookService, WebhookService>();
|
||||
services.AddHttpClient<IWebhookService, WebhookService>();
|
||||
services.AddTransient<IKaigenService, KaigenService>();
|
||||
services.AddTransient<IWhitelistService, WhitelistService>();
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 <T>(
|
||||
): Promise<T> => {
|
||||
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<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user