Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"setup": "bash ./gateway-setup.sh",
"setup:with-defaults": "bash ./gateway-setup.sh --with-defaults",
"start": "START_SERVER=true node dist/index.js",
"copy-files": "copyfiles 'src/templates/namespace/*.json' 'src/templates/*.yml' 'src/templates/chains/**/*.yml' 'src/templates/connectors/*.yml' 'src/templates/tokens/**/*.json' 'src/templates/pools/*.json' dist && copyfiles -u 1 'src/connectors/pancakeswap-sol/idl/*.json' dist",
"copy-files": "copyfiles 'src/templates/namespace/*.json' 'src/templates/*.yml' 'src/templates/chains/**/*.yml' 'src/templates/connectors/*.yml' 'src/templates/tokens/**/*.json' 'src/templates/pools/**/*.json' dist && copyfiles -u 1 'src/connectors/pancakeswap-sol/idl/*.json' dist",
"test": "GATEWAY_TEST_MODE=dev jest --verbose",
"test:clear-cache": "jest --clearCache",
"test:debug": "GATEWAY_TEST_MODE=dev jest --watch --runInBand",
Expand Down
91 changes: 43 additions & 48 deletions src/connectors/meteora/clmm-routes/fetchPools.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,67 @@
import { Type } from '@sinclair/typebox';
import { FastifyPluginAsync } from 'fastify';

import { Solana } from '../../../chains/solana/solana';
import { PoolInfo, PoolInfoSchema, FetchPoolsRequestType } from '../../../schemas/clmm-schema';
import { FetchPoolsResponse } from '../../../schemas/clmm-schema';
import { logger } from '../../../services/logger';
import { Meteora } from '../meteora';
import { Meteora, MeteoraApiPool } from '../meteora';
import { MeteoraClmmFetchPoolsRequest } from '../schemas';
// Using Fastify's native error handling

export const fetchPoolsRoute: FastifyPluginAsync = async (fastify) => {
fastify.get<{
Querystring: FetchPoolsRequestType;
Reply: PoolInfo[];
Querystring: {
network?: string;
page?: number;
limit?: number;
query?: string;
sortBy?: string;
includeUnverified?: boolean;
};
}>('/fetch-pools', {
schema: {
description: 'Fetch info about Meteora pools',
description: 'Fetch Meteora pools from API with search and sorting',
tags: ['/connector/meteora'],
querystring: MeteoraClmmFetchPoolsRequest,
response: {
200: Type.Array(PoolInfoSchema),
200: FetchPoolsResponse,
},
},
handler: async (request, _reply) => {
try {
const { limit, tokenA, tokenB } = request.query;
const network = request.query.network;
const { network, page, limit, query, sortBy, includeUnverified } = request.query;

const meteora = await Meteora.getInstance(network);
const solana = await Solana.getInstance(network);

let tokenMintA, tokenMintB;
const result = await meteora.fetchPoolsFromApi({
page,
limit,
query,
sortBy,
includeUnverified,
});

if (tokenA) {
const tokenInfoA = await solana.getToken(tokenA);
if (!tokenInfoA) {
throw fastify.httpErrors.notFound(`Token ${tokenA} not found`);
}
tokenMintA = tokenInfoA.address;
}
// Map API response to simplified format
const pools = result.pools.map((pool: MeteoraApiPool) => ({
address: pool.address,
name: pool.name,
baseTokenAddress: pool.token_x.address,
baseTokenSymbol: pool.token_x.symbol,
quoteTokenAddress: pool.token_y.address,
quoteTokenSymbol: pool.token_y.symbol,
binStep: pool.pool_config.bin_step,
baseFee: pool.pool_config.base_fee_pct,
price: pool.current_price,
tvl: pool.tvl,
apr: pool.apr,
apy: pool.apy,
volume24h: pool.volume?.['24h'],
fees24h: pool.fees?.['24h'],
}));

if (tokenB) {
const tokenInfoB = await solana.getToken(tokenB);
if (!tokenInfoB) {
throw fastify.httpErrors.notFound(`Token ${tokenB} not found`);
}
tokenMintB = tokenInfoB.address;
}

const pairs = await meteora.getPools(limit, tokenMintA, tokenMintB);
if (!Array.isArray(pairs)) {
logger.error('No matching Meteora pools found');
return [];
}

const poolInfos = await Promise.all(
pairs
.filter((pair) => pair?.publicKey?.toString)
.map(async (pair) => {
try {
return await meteora.getPoolInfo(pair.publicKey.toString());
} catch (error) {
logger.error(`Failed to get pool info for ${pair.publicKey.toString()}: ${error.message}`);
throw fastify.httpErrors.notFound(`Pool not found: ${pair.publicKey.toString()}`);
}
}),
);

return poolInfos.filter(Boolean);
return {
pools,
total: result.total,
page: result.page,
pageSize: result.pageSize,
};
} catch (e) {
logger.error('Error in fetch-pools:', e);
if (e.statusCode) throw e;
Expand Down
107 changes: 106 additions & 1 deletion src/connectors/meteora/meteora.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,50 @@ import { logger } from '../../services/logger';

import { MeteoraConfig } from './meteora.config';

/** Pool data from Meteora API */
export interface MeteoraApiPool {
address: string;
name: string;
token_x: {
address: string;
symbol: string;
name: string;
decimals: number;
is_verified: boolean;
price?: number;
};
token_y: {
address: string;
symbol: string;
name: string;
decimals: number;
is_verified: boolean;
price?: number;
};
reserve_x: string;
reserve_y: string;
token_x_amount: number;
token_y_amount: number;
pool_config: {
bin_step: number;
base_fee_pct: number;
max_fee_pct: number;
protocol_fee_pct: number;
};
dynamic_fee_pct: number;
tvl: number;
current_price: number;
apr: number;
apy: number;
has_farm: boolean;
farm_apr: number;
farm_apy: number;
volume: { '30m'?: number; '1h'?: number; '24h'?: number };
fees: { '30m'?: number; '1h'?: number; '24h'?: number };
is_blacklisted: boolean;
tags?: string[];
}

export class Meteora {
private static _instances: { [name: string]: Meteora };
// Recommended maximum bins per position (aligns with SDK's DEFAULT_BIN_PER_POSITION)
Expand Down Expand Up @@ -91,7 +135,7 @@ export class Meteora {
tokenMintA?: string,
tokenMintB?: string,
): Promise<{ publicKey: PublicKey; account: LbPair }[]> {
const timeoutMs = 10000;
const timeoutMs = 60000; // Increased to 60s - fetching all pools from blockchain is slow
try {
logger.info('Fetching Meteora pools...');
const lbPairsPromise = DLMM.getLbPairs(this.solana.connection, {
Expand Down Expand Up @@ -136,6 +180,67 @@ export class Meteora {
}
}

/** Fetches pools from Meteora API (fast, paginated) */
async fetchPoolsFromApi(
options: {
page?: number;
limit?: number;
query?: string;
sortBy?: string;
includeUnverified?: boolean;
} = {},
): Promise<{
pools: MeteoraApiPool[];
total: number;
page: number;
pageSize: number;
}> {
const { page = 0, limit = 50, query, sortBy = 'volume_24h:desc', includeUnverified = true } = options;

try {
const url = new URL('https://dlmm.datapi.meteora.ag/pools');
url.searchParams.set('page', String(page + 1)); // API uses 1-based pagination
url.searchParams.set('page_size', String(Math.min(limit, 1000)));

if (query) {
url.searchParams.set('query', query);
}
if (sortBy) {
url.searchParams.set('sort_by', sortBy);
}

// Filter out blacklisted pools
const filters = ['is_blacklisted=false'];
if (!includeUnverified) {
filters.push('token_x.is_verified=true');
filters.push('token_y.is_verified=true');
}
url.searchParams.set('filter_by', filters.join(' && '));

logger.info(`Fetching Meteora pools from API: ${url.toString()}`);

const response = await fetch(url.toString(), {
headers: { accept: 'application/json' },
});

if (!response.ok) {
throw new Error(`Meteora API error: ${response.status} ${response.statusText}`);
}

const data = await response.json();

return {
pools: data.data || [],
total: data.total || 0,
page: data.current_page || 1,
pageSize: data.page_size || limit,
};
} catch (error) {
logger.error('Failed to fetch pools from Meteora API:', error);
throw error;
}
}

/** Gets comprehensive pool information */
async getPoolInfo(poolAddress: string): Promise<MeteoraPoolInfo | null> {
try {
Expand Down
34 changes: 25 additions & 9 deletions src/connectors/meteora/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,24 +365,40 @@ export const MeteoraClmmFetchPoolsRequest = Type.Object({
enum: [...MeteoraConfig.networks],
}),
),
page: Type.Optional(
Type.Number({
minimum: 0,
default: 0,
description: 'Page number (0-based)',
examples: [0],
}),
),
limit: Type.Optional(
Type.Number({
minimum: 1,
default: 10,
description: 'Maximum number of pools to return',
examples: [10],
maximum: 1000,
default: 50,
description: 'Maximum number of pools to return (max 1000)',
examples: [50],
}),
),
tokenA: Type.Optional(
query: Type.Optional(
Type.String({
description: 'First token symbol or address',
examples: [BASE_TOKEN],
description: 'Search query to match pools by name, tokens, or address',
examples: ['SOL', 'USDC', 'SOL-USDC'],
}),
),
tokenB: Type.Optional(
sortBy: Type.Optional(
Type.String({
description: 'Second token symbol or address',
examples: [QUOTE_TOKEN],
description: 'Sort by field (volume, fees, tvl, apr) with optional time window',
default: 'volume_24h:desc',
examples: ['volume_24h:desc', 'tvl:desc', 'apr:desc'],
}),
),
includeUnverified: Type.Optional(
Type.Boolean({
description: 'Include pools with unverified tokens',
default: true,
}),
),
});
Expand Down
Loading
Loading