Add new endpoint to retrieve balance

This commit is contained in:
2025-08-15 20:18:02 +07:00
parent cd93dede4e
commit b178f15beb
4 changed files with 393 additions and 26 deletions

View File

@@ -11,7 +11,7 @@ import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js'
import {TOKENS} from '../../generated/gmxsdk/configs/tokens.js'
import {CONTRACTS} from '../../generated/gmxsdk/configs/contracts.js'
import {getClientForAddress, getTokenDataFromTicker} from './gmx.js'
import {Ticker} from '../../generated/ManagingApiTypes.js'
import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js'
import {Address} from 'viem'
// Load environment variables
@@ -45,6 +45,7 @@ declare module 'fastify' {
approveToken: typeof approveToken;
initAddress: typeof initAddress;
sendToken: typeof sendToken;
getWalletBalance: typeof getWalletBalance;
}
}
@@ -65,6 +66,10 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
throw new Error('Missing required Privy environment variables');
}
console.log('appId', appId)
console.log('appSecret', appSecret)
console.log('authKey', authKey)
return new PrivyClient(
appId,
appSecret,
@@ -117,11 +122,17 @@ export function getAuthorizationSignature({url, body}: {url: string; body: objec
/**
* Makes a request to the Privy API with proper authentication and headers.
* @param url - The full URL for the API endpoint.
* @param body - The request body.
* @param body - The request body (optional for GET requests).
* @param requiresAuth - Whether the request requires authentication.
* @param method - The HTTP method (defaults to POST).
* @returns The response data.
*/
export const makePrivyRequest = async <T>(url: string, body: object, requiresAuth = true): Promise<T> => {
export const makePrivyRequest = async <T>(
url: string,
body: object = {},
requiresAuth = true,
method: 'GET' | 'POST' = 'POST'
): Promise<T> => {
try {
let headers: Record<string, string> = {
'Content-Type': 'application/json',
@@ -129,31 +140,31 @@ export const makePrivyRequest = async <T>(url: string, body: object, requiresAut
};
if (requiresAuth) {
// Generate authorization signature
const authSig = await getAuthorizationSignature({
url,
body
});
// Create authentication string for basic auth
const basicAuthString = `${process.env.PRIVY_APP_ID}:${process.env.PRIVY_APP_SECRET}`;
// Create Basic Authentication header
const appId = process.env.PRIVY_APP_ID ?? "";
const appSecret = process.env.PRIVY_APP_SECRET ?? "";
const basicAuthString = `${appId}:${appSecret}`;
const base64Auth = Buffer.from(basicAuthString).toString('base64');
headers = {
...headers,
'privy-authorization-signature': authSig,
'Authorization': `Basic ${base64Auth}`
};
headers.Authorization = `Basic ${base64Auth}`;
}
const request = {
method: 'POST',
const requestInit: RequestInit = {
method: method,
headers,
body: JSON.stringify(body)
};
// Only add body for non-GET requests
if (method !== 'GET') {
requestInit.body = JSON.stringify(body);
}
console.log('url', url)
console.log('requestInit', requestInit)
// Make the API request
const response = await fetch(url, request).then(res => {
const response = await fetch(url, requestInit).then(res => {
return res;
}).catch(err => {
console.log("error", err);
@@ -196,6 +207,13 @@ const tokenSendSchema = z.object({
chainId: z.number().positive().optional()
});
// Schema for wallet balance request
const walletBalanceSchema = z.object({
address: z.string().nonempty(),
assets: z.array(z.nativeEnum(Ticker)).min(1), // Required: array of valid Ticker enum values
chains: z.array(z.string().nonempty()).min(1) // Required: array of chain names
});
/**
* Gets the chain name based on chain ID
* @param chainId The chain ID
@@ -732,6 +750,228 @@ export const sendTokenImpl = async (
}
};
/**
* Gets the wallet balance for a specific address using Privy API
* @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'])
* @returns Array of Balance objects
*/
export const getWalletBalanceImpl = async (
address: string,
assets: Ticker[],
chains: string[]
): Promise<Balance[]> => {
try {
// First, get the user by wallet address using Privy Client
const privy = getPrivyClient();
// Get user by wallet address
const user = await privy.getUserByWalletAddress(address);
if (!user) {
throw new Error(`User not found for wallet address: ${address}`);
}
// Find the embedded wallet for this address
const embeddedWallet = user.linkedAccounts.find(
(account: any) =>
account.type === 'wallet' &&
account.chainType === 'ethereum' &&
account.address?.toLowerCase() === address.toLowerCase()
);
if (!embeddedWallet) {
throw new Error(`Embedded wallet not found for address: ${address}`);
}
// Extract wallet ID from the embedded wallet
const walletId = (embeddedWallet as any).walletId || (embeddedWallet as any).id;
if (!walletId) {
throw new Error(`Wallet ID not found for embedded wallet`);
}
// Make authenticated request to get wallet balance
let balanceUrl = `https://auth.privy.io/api/v1/wallets/${walletId}/balance`;
// Add required query parameters for multiple assets and chains
const queryParams = new URLSearchParams();
// Add multiple asset parameters
assets.forEach(asset => {
queryParams.append('asset', asset.toLowerCase()); // Convert Ticker enum to lowercase
});
// Add multiple chain parameters
chains.forEach(chain => {
queryParams.append('chain', chain);
});
// Add include_currency parameter for USD pricing
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;
};
}>;
}>(balanceUrl, {}, true, 'GET');
console.log('balanceResponse', balanceResponse)
// 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);
// Get USD price from display_values
const usdValue = parseFloat(balance.display_values.usd);
// Calculate price per token (if amount > 0)
const price = amount > 0 ? usdValue / amount : 0;
// Get chain ID from chain name
const chainId = getChainIdFromName(balance.chain);
const caip2 = getChainName(chainId);
const chain: Chain = {
id: caip2,
rpcUrl: undefined,
name: getChainNameFromCaip2(caip2),
chainId: chainId
};
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
};
});
console.log('balances', balances)
return balances;
} catch (error) {
console.error('Error getting wallet balance:', error);
throw new Error(`Failed to get wallet balance: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Helper function to get chain name from CAIP-2 identifier
* @param caip2 The CAIP-2 identifier
* @returns Human readable chain name
*/
const getChainNameFromCaip2 = (caip2: string): string => {
switch (caip2) {
case 'eip155:1':
return 'Ethereum Mainnet';
case 'eip155:42161':
return 'Arbitrum One';
case 'eip155:421613':
return 'Arbitrum Goerli';
case 'eip155:8453':
return 'Base';
case 'eip155:84531':
return 'Base Goerli';
default:
return `Chain ${caip2}`;
}
};
/**
* Helper function to get chain ID from chain name
* @param chainName The chain name (e.g., 'arbitrum', 'ethereum', 'base')
* @returns The corresponding chain ID
*/
const getChainIdFromName = (chainName: string): number => {
switch (chainName.toLowerCase()) {
case 'ethereum':
case 'mainnet':
return 1;
case 'arbitrum':
case 'arbitrum-one':
return 42161;
case 'arbitrum-goerli':
return 421613;
case 'base':
return 8453;
case 'base-goerli':
return 84531;
default:
// Default to Arbitrum if unknown chain
return 42161;
}
};
/**
* Gets wallet balance using Privy embedded wallet
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @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'])
* @returns The response object with success status and balances array
*/
export async function getWalletBalance(
this: FastifyRequest,
reply: FastifyReply,
address: string,
assets: Ticker[],
chains: string[]
) {
try {
// Validate the request parameters
walletBalanceSchema.parse({
address,
assets,
chains
});
if (!address) {
throw new Error('Wallet address is required for balance retrieval');
}
if (!assets || assets.length === 0) {
throw new Error('At least one asset is required for balance retrieval');
}
if (!chains || chains.length === 0) {
throw new Error('At least one chain is required for balance retrieval');
}
// Call the getWalletBalanceImpl function
const balances = await getWalletBalanceImpl(address, assets, chains);
return {
success: true,
balances: balances
};
} catch (error) {
this.log.error(error);
// Return appropriate error response
reply.status(error instanceof z.ZodError ? 400 : 500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* Sends tokens from one address to another using Privy wallet
* @param this The FastifyRequest instance
@@ -821,6 +1061,10 @@ export default fp(async (fastify) => {
return sendToken.call(this, reply, senderAddress, recipientAddress, ticker, amount, chainId);
});
fastify.decorateRequest('getWalletBalance', async function(this: FastifyRequest, reply: FastifyReply, address: string, assets: Ticker[], chains: string[]) {
return getWalletBalance.call(this, reply, address, assets, chains);
});
// Test the Privy client initialization
try {
const testClient = getPrivyClient(fastify);