Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix DeFi Positions fetching and transforming #2718

Merged
merged 3 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions functions/balances/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const endpoints = {
transform: transformDefiResponse,
fetch: async ({ chain, address }: { chain: string; address: string }, c: { env: Env }) => {
const result = await fetchMoralis<DefiResponse>({
isPaginated: false,
endpoint: endpoints.defi.moralisPath(address),
chain,
apiKey: c.env.MORALIS_API_KEY,
Expand Down
31 changes: 30 additions & 1 deletion functions/balances/transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,34 @@ export function transformNFTResponse(nft: NFTResponse): NFTBalance {
}

export function transformDefiResponse(defi: DefiResponse): DefiBalance {
return defi;
return {
protocolName: defi.protocol_name,
protocolId: defi.protocol,
protocolUrl: defi.protocol_url,
protocolLogo: defi.protocol_logo,
position: {
label: defi.position.label || '',
tokens: (defi.position.tokens || []).map(token => ({
contractAddress: token.contract_address,
tokenType: token.token_type as 'supplied' | 'defi-token',
symbol: token.symbol,
name: token.name,
decimals: token.decimals,
balance: token.balance,
balanceFormatted: token.balance_formatted,
logo: token.logo,
thumbnail: token.thumbnail,
usdPrice: token.usd_price,
usdValue: token.usd_value,
nativeToken: false,
portfolioPercentage: 0,
tokenAddress: token.contract_address || '',
verifiedContract: true,
})),
address: defi.position.address,
balanceUsd: defi.position.balance_usd || 0,
totalUnclaimedUsdValue: defi.position.total_unclaimed_usd_value || 0,
positionDetails: defi.position.position_details || undefined,
},
};
}
98 changes: 75 additions & 23 deletions functions/shared/moralisApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,60 +13,112 @@ interface MoralisRequestConfig {
chain: string;
apiKey: string;
params?: Record<string, string>;
isPaginated?: boolean;
}

export async function fetchMoralis<T>({
endpoint,
chain,
apiKey,
params = {},
isPaginated = true,
}: MoralisRequestConfig): Promise<T[]> {
let allResults: T[] = [];
let cursor: string | null = null;
let pageCount = 0;
const limit = 100;
const MAX_PAGES = 10; // Safeguard against infinite loops

do {
pageCount++;
const chainHex = toHex(parseInt(chain));
const url = new URL(`https://deep-index.moralis.io/api/v2.2${endpoint}`);

// Add chain parameter
url.searchParams.append('chain', chainHex);

// Add cursor if available
if (cursor) {
url.searchParams.append('cursor', cursor);
// Add cursor and limit only for paginated endpoints
if (isPaginated) {
if (cursor) {
url.searchParams.append('cursor', cursor);
}
url.searchParams.append('limit', limit.toString());
}

// Add limit
url.searchParams.append('limit', limit.toString());

// Add any additional parameters
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});

const response = await fetch(url, {
headers: {
Accept: 'application/json',
'x-api-key': apiKey,
},
method: 'GET',
});
let response;
try {
response = await fetch(url, {
headers: {
Accept: 'application/json',
'x-api-key': apiKey,
},
method: 'GET',
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
if (!response.ok) {
console.error('Moralis API HTTP error:', {
endpoint,
status: response.status,
statusText: response.statusText,
chain: chainHex,
params,
});
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('Moralis API fetch error:', {
endpoint,
chain: chainHex,
error: error instanceof Error ? error.message : 'Unknown error',
params,
});
throw error;
}

const data: MoralisResponse<T> = await response.json();
const data = await response.json();

// Handle no results
if (!data || !data.result) {
break;
}
// Handle paginated vs non-paginated responses
if (isPaginated) {
const paginatedData = data as MoralisResponse<T>;
if (!paginatedData || !paginatedData.result) {
console.error('Unexpected Moralis API response structure:', {
endpoint,
responseData: data,
isPaginatedButMissingRequiredFields: true,
});
break;
}
allResults = allResults.concat(paginatedData.result);
cursor = paginatedData.cursor;

allResults = allResults.concat(data.result);
cursor = data.cursor;
} while (cursor !== null);
// Log if we're hitting pagination limits
if (pageCount >= MAX_PAGES && cursor !== null) {
console.warn('Moralis API pagination limit reached:', {
endpoint,
pageCount,
totalResultsCount: allResults.length,
hasMoreData: true,
});
break;
}
} else {
// For non-paginated endpoints, data is the direct array
if (!Array.isArray(data)) {
console.error('Unexpected Moralis API response structure:', {
endpoint,
responseData: data,
expectedArray: true,
});
return [];
}
return data;
}
} while (cursor !== null && isPaginated);

return allResults;
}
38 changes: 30 additions & 8 deletions functions/shared/moralisTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DefiPosition, NFTMedia } from '../../src/types/daoTreasury';
import { NFTMedia } from '../../src/types/daoTreasury';

export type TokenResponse = {
balance: string;
Expand Down Expand Up @@ -54,11 +54,33 @@ export type NFTResponse = {
floor_price_currency: string | null;
};

export type DefiResponse = {
chain: string;
interface DefiTokenResponse {
contract_address?: string;
token_type: 'supplied' | 'defi-token';
symbol: string;
name: string;
decimals: number;
balance: string;
balance_formatted: string;
logo?: string;
thumbnail?: string;
usd_price?: number;
usd_value?: number;
}

interface DefiPositionResponse {
label: string;
tokens: DefiTokenResponse[];
address?: string;
balance_usd: number;
total_unclaimed_usd_value: number;
position_details?: Record<string, any>;
}

export interface DefiResponse {
protocol_name: string;
protocol: string;
protocolId: string;
protocolUrl: string;
protocolLogo: string;
position: DefiPosition;
};
protocol_url: string;
protocol_logo: string;
position: DefiPositionResponse;
}