Add new endpoint to retrieve balance
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user