Enhance Privy API integration with fallback RPC support for wallet balance retrieval
- Added an AbortSignal parameter to the makePrivyRequest function to support request cancellation. - Improved error handling in makePrivyRequest to preserve detailed error information for better debugging. - Implemented a fallback mechanism in getWalletBalanceImpl to retrieve wallet balances via direct RPC calls when Privy API fails (e.g., 503 errors). - Introduced a new getWalletBalanceViaRpc function to handle RPC balance retrieval, including detailed logging and error management. - Enhanced overall error messaging to provide clearer feedback during balance retrieval processes.
This commit is contained in:
@@ -177,7 +177,8 @@ export const makePrivyRequest = async <T>(
|
||||
body: object = {},
|
||||
requiresAuth = true,
|
||||
method: 'GET' | 'POST' = 'POST',
|
||||
fastify?: FastifyInstance
|
||||
fastify?: FastifyInstance,
|
||||
signal?: AbortSignal
|
||||
): Promise<T> => {
|
||||
try {
|
||||
// Resolve secrets from cached memory (loaded once at startup from Infisical in production)
|
||||
@@ -204,6 +205,7 @@ export const makePrivyRequest = async <T>(
|
||||
const requestInit: RequestInit = {
|
||||
method: method,
|
||||
headers,
|
||||
signal, // Add abort signal support
|
||||
};
|
||||
|
||||
// Only add body for non-GET requests
|
||||
@@ -220,12 +222,21 @@ export const makePrivyRequest = async <T>(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Privy API request failed: ${response.status}`);
|
||||
const errorText = await response.text().catch(() => '');
|
||||
const error = new Error(`Privy API request failed: ${response.status}`);
|
||||
(error as any).status = response.status;
|
||||
(error as any).statusCode = response.status;
|
||||
(error as any).response = { status: response.status, text: errorText };
|
||||
throw error;
|
||||
}
|
||||
|
||||
return await response.json() as T;
|
||||
} catch (error) {
|
||||
console.error('Error making Privy API request:', error);
|
||||
// Preserve error details for fallback logic
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error('Failed to make Privy API request');
|
||||
}
|
||||
};
|
||||
@@ -1084,8 +1095,155 @@ export const sendTokenImpl = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fallback function to get wallet balances using direct RPC calls
|
||||
* Used when Privy API is unavailable (503, timeout, etc.)
|
||||
*/
|
||||
const getWalletBalanceViaRpc = async (
|
||||
address: string,
|
||||
assets: Ticker[],
|
||||
chains: string[],
|
||||
fastify?: FastifyInstance
|
||||
): Promise<Balance[]> => {
|
||||
console.log('🔄 Falling back to RPC calls for wallet balance...');
|
||||
|
||||
const balances: Balance[] = [];
|
||||
|
||||
try {
|
||||
// Get GMX SDK client for RPC calls
|
||||
const sdk = await getClientForAddress(address);
|
||||
|
||||
// Get markets info to access token prices
|
||||
const { tokensData } = await sdk.markets.getMarketsInfo();
|
||||
|
||||
if (!tokensData) {
|
||||
throw new Error('Failed to get tokens data for price lookup');
|
||||
}
|
||||
|
||||
// Process each chain
|
||||
for (const chainName of chains) {
|
||||
const chainId = getChainIdFromName(chainName);
|
||||
if (!chainId) {
|
||||
console.warn(`⚠️ Unknown chain: ${chainName}, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get tokens for this chain
|
||||
const chainTokens = TOKENS[chainId];
|
||||
if (!chainTokens) {
|
||||
console.warn(`⚠️ No tokens configured for chain ${chainId}, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process each asset
|
||||
for (const asset of assets) {
|
||||
const ticker = asset.toString().toUpperCase();
|
||||
|
||||
// Find token in chain tokens
|
||||
const token = chainTokens.find(t =>
|
||||
t.symbol === ticker ||
|
||||
t.assetSymbol === ticker ||
|
||||
t.baseSymbol === ticker
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
console.warn(`⚠️ Token ${ticker} not found on chain ${chainId}, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get balance via RPC
|
||||
let rawBalance: bigint;
|
||||
|
||||
if (token.isNative) {
|
||||
// For native tokens (ETH), get balance directly
|
||||
rawBalance = await sdk.publicClient.getBalance({
|
||||
address: address as Address
|
||||
});
|
||||
} else {
|
||||
// For ERC20 tokens, use multicall
|
||||
const balanceResult = await sdk.executeMulticall({
|
||||
token: {
|
||||
contractAddress: token.address,
|
||||
abiId: "Token",
|
||||
calls: {
|
||||
balance: {
|
||||
methodName: "balanceOf",
|
||||
params: [address]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rawBalance = balanceResult?.data?.token?.balance?.returnValues?.[0];
|
||||
|
||||
if (rawBalance === undefined || rawBalance === null) {
|
||||
console.warn(`⚠️ Failed to get balance for ${ticker} on ${chainName}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to decimal format
|
||||
const decimals = token.decimals || 18;
|
||||
const amount = Number(rawBalance) / Math.pow(10, decimals);
|
||||
|
||||
// Get USD price from tokens data
|
||||
const tokenData = tokensData[token.address];
|
||||
let usdValue = 0;
|
||||
let price = 0;
|
||||
|
||||
if (tokenData?.prices?.minPrice) {
|
||||
// GMX prices are in 30 decimals
|
||||
const tokenPriceUsd = Number(tokenData.prices.minPrice) / 1e30;
|
||||
price = tokenPriceUsd;
|
||||
usdValue = amount * tokenPriceUsd;
|
||||
} else {
|
||||
// Fallback: try to get price from token config
|
||||
console.warn(`⚠️ No price data for ${ticker}, USD value will be 0`);
|
||||
}
|
||||
|
||||
// Build chain info
|
||||
const caip2 = getChainName(chainId);
|
||||
const chain: Chain = {
|
||||
id: caip2,
|
||||
rpcUrl: undefined,
|
||||
name: getChainNameFromCaip2(caip2),
|
||||
chainId: chainId
|
||||
};
|
||||
|
||||
balances.push({
|
||||
tokenImage: '',
|
||||
tokenName: ticker,
|
||||
amount: amount,
|
||||
price: price,
|
||||
value: usdValue,
|
||||
tokenAdress: token.address,
|
||||
chain: chain
|
||||
});
|
||||
|
||||
console.log(`✅ RPC balance for ${ticker} on ${chainName}: ${amount.toFixed(6)} ($${usdValue.toFixed(2)})`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error getting RPC balance for ${ticker} on ${chainName}:`, error);
|
||||
// Continue with other assets/chains
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (balances.length === 0) {
|
||||
throw new Error('No balances found via RPC fallback');
|
||||
}
|
||||
|
||||
console.log(`✅ RPC fallback successful: found ${balances.length} balance(s)`);
|
||||
return balances;
|
||||
} catch (error) {
|
||||
console.error('❌ RPC fallback failed:', error);
|
||||
throw new Error(`RPC fallback failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the wallet balance for a specific address using Privy API
|
||||
* Falls back to RPC calls if Privy API fails
|
||||
* @param address The wallet address to get balance for
|
||||
* @param assets Array of assets to filter by using Ticker enum (e.g. [Ticker.USDC, Ticker.ETH])
|
||||
* @param chains Array of chain names to filter by (e.g. ['arbitrum', 'ethereum'])
|
||||
@@ -1098,6 +1256,7 @@ export const getWalletBalanceImpl = async (
|
||||
fastify?: FastifyInstance
|
||||
): Promise<Balance[]> => {
|
||||
try {
|
||||
// Try Privy API first
|
||||
// First, get the user by wallet address using Privy Client
|
||||
const privy = getPrivyClient(fastify);
|
||||
|
||||
@@ -1135,7 +1294,7 @@ export const getWalletBalanceImpl = async (
|
||||
|
||||
// Add multiple asset parameters
|
||||
assets.forEach(asset => {
|
||||
queryParams.append('asset', asset.toLowerCase()); // Convert Ticker enum to lowercase
|
||||
queryParams.append('asset', asset.toLowerCase());
|
||||
});
|
||||
|
||||
// Add multiple chain parameters
|
||||
@@ -1147,57 +1306,103 @@ export const getWalletBalanceImpl = async (
|
||||
queryParams.append('include_currency', 'usd');
|
||||
|
||||
balanceUrl += `?${queryParams.toString()}`;
|
||||
const balanceResponse = await makePrivyRequest<{
|
||||
balances: Array<{
|
||||
chain: string;
|
||||
asset: string;
|
||||
raw_value: string;
|
||||
raw_value_decimals: number;
|
||||
display_values: {
|
||||
[key: string]: string;
|
||||
usd: string;
|
||||
|
||||
// Add timeout to Privy API call (25 seconds to leave buffer before HttpClient timeout)
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 25000);
|
||||
|
||||
try {
|
||||
const balanceResponse = await makePrivyRequest<{
|
||||
balances: Array<{
|
||||
chain: string;
|
||||
asset: string;
|
||||
raw_value: string;
|
||||
raw_value_decimals: number;
|
||||
display_values: {
|
||||
[key: string]: string;
|
||||
usd: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}>(balanceUrl, {}, true, 'GET', fastify);
|
||||
}>(balanceUrl, {}, true, 'GET', fastify, controller.signal);
|
||||
|
||||
// Convert to Balance interface format (matching ManagingApiTypes.ts)
|
||||
const balances: Balance[] = balanceResponse.balances
|
||||
.map(balance => {
|
||||
// Parse the raw value using decimals
|
||||
const amount = parseFloat(balance.raw_value) / Math.pow(10, balance.raw_value_decimals);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Get USD price from display_values
|
||||
const usdValue = parseFloat(balance.display_values.usd);
|
||||
// Convert to Balance interface format
|
||||
const balances: Balance[] = balanceResponse.balances
|
||||
.map(balance => {
|
||||
const amount = parseFloat(balance.raw_value) / Math.pow(10, balance.raw_value_decimals);
|
||||
const usdValue = parseFloat(balance.display_values.usd);
|
||||
const price = amount > 0 ? usdValue / amount : 0;
|
||||
const chainId = getChainIdFromName(balance.chain);
|
||||
const caip2 = getChainName(chainId);
|
||||
|
||||
// Calculate price per token (if amount > 0)
|
||||
const price = amount > 0 ? usdValue / amount : 0;
|
||||
const chain: Chain = {
|
||||
id: caip2,
|
||||
rpcUrl: undefined,
|
||||
name: getChainNameFromCaip2(caip2),
|
||||
chainId: chainId
|
||||
};
|
||||
|
||||
// Get chain ID from chain name
|
||||
const chainId = getChainIdFromName(balance.chain);
|
||||
const caip2 = getChainName(chainId);
|
||||
return {
|
||||
tokenImage: '',
|
||||
tokenName: balance.asset.toUpperCase(),
|
||||
amount: amount,
|
||||
price: price,
|
||||
value: usdValue,
|
||||
tokenAdress: '',
|
||||
chain: chain
|
||||
};
|
||||
});
|
||||
|
||||
const chain: Chain = {
|
||||
id: caip2,
|
||||
rpcUrl: undefined,
|
||||
name: getChainNameFromCaip2(caip2),
|
||||
chainId: chainId
|
||||
};
|
||||
return balances;
|
||||
} catch (privyError: any) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
return {
|
||||
tokenImage: '', // Not provided in the response
|
||||
tokenName: balance.asset.toUpperCase(), // Use asset name (usdc -> USDC)
|
||||
amount: amount,
|
||||
price: price,
|
||||
value: usdValue,
|
||||
tokenAdress: '', // Not provided in this response format
|
||||
chain: chain
|
||||
};
|
||||
});
|
||||
// Check if this is a retryable error (503, 500, timeout, network error)
|
||||
const errorMessage = privyError?.message || String(privyError);
|
||||
const statusCode = privyError?.status || privyError?.statusCode || privyError?.response?.status;
|
||||
|
||||
return balances;
|
||||
const isRetryableError =
|
||||
statusCode === 503 ||
|
||||
statusCode === 500 ||
|
||||
statusCode === 502 ||
|
||||
statusCode === 504 ||
|
||||
errorMessage.includes('503') ||
|
||||
errorMessage.includes('500') ||
|
||||
errorMessage.includes('timeout') ||
|
||||
errorMessage.includes('aborted') ||
|
||||
errorMessage.includes('ECONNRESET') ||
|
||||
errorMessage.includes('ENOTFOUND') ||
|
||||
errorMessage.includes('Cloudflare') ||
|
||||
errorMessage.includes('Internal server error');
|
||||
|
||||
if (isRetryableError) {
|
||||
console.warn(`⚠️ Privy API error (${statusCode || 'unknown'}), falling back to RPC calls...`);
|
||||
console.warn(`Error details: ${errorMessage}`);
|
||||
|
||||
// Fall back to RPC calls
|
||||
return await getWalletBalanceViaRpc(address, assets, chains, fastify);
|
||||
}
|
||||
|
||||
// For non-retryable errors (like 404, 401), throw immediately
|
||||
throw privyError;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting wallet balance:', error);
|
||||
throw new Error(`Failed to get wallet balance: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
// Final fallback: if everything fails, try RPC
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Only fallback if it's not already an RPC error
|
||||
if (!errorMessage.includes('RPC fallback failed')) {
|
||||
console.warn(`⚠️ Primary method failed, attempting RPC fallback...`);
|
||||
try {
|
||||
return await getWalletBalanceViaRpc(address, assets, chains, fastify);
|
||||
} catch (rpcError) {
|
||||
console.error('❌ Both Privy API and RPC fallback failed');
|
||||
throw new Error(`Failed to get wallet balance: ${errorMessage}. RPC fallback also failed: ${rpcError instanceof Error ? rpcError.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user