Skip to content
Draft
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 && copyfiles -u 1 'src/connectors/metadao/*.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
112 changes: 112 additions & 0 deletions scripts/fetch-metadao-daos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Fetch all MetaDAO pools/DAOs and save to a JSON file.
* Run with: npx ts-node scripts/fetch-metadao-daos.ts
* Requires server running: pnpm start --passphrase=test --dev
*/

const BASE_URL = 'http://localhost:15888';
const DELAY_MS = 20000; // Delay between requests (20s to avoid rate limits)

// Use any type since we want to save all fields from the API response
type DaoInfo = Record<string, unknown>;

async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchDaoList(): Promise<string[]> {
const response = await fetch(`${BASE_URL}/connectors/metadao/futarchy/daos`);
if (!response.ok) {
throw new Error(`Failed to fetch DAO list: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.daos;
}

async function fetchDaoInfo(address: string): Promise<DaoInfo | null> {
try {
const url = `${BASE_URL}/connectors/metadao/futarchy/dao-info?pool=${encodeURIComponent(address)}`;
const response = await fetch(url);
if (!response.ok) {
console.error(`Failed to fetch DAO info for ${address}: ${response.status}`);
return null;
}
return await response.json();
} catch (error) {
console.error(`Error fetching DAO info for ${address}:`, error);
return null;
}
}

async function saveToken(address: string): Promise<void> {
try {
const url = `${BASE_URL}/tokens/save/${encodeURIComponent(address)}?chainNetwork=solana-mainnet-beta`;
const response = await fetch(url, { method: 'POST' });
if (!response.ok) {
console.warn(` -> token save skipped for ${address}: ${response.status}`);
return;
}
const data = await response.json();
console.log(` -> token saved/found: ${data.token?.symbol || address}`);
} catch (error) {
console.warn(` -> token save failed for ${address}: ${error}`);
}
}

async function main() {
console.log('Fetching MetaDAO DAO list...');
const allDaos = await fetchDaoList();
const existingDaos = [...new Set(allDaos)];
console.log(`Found ${allDaos.length} DAOs (${existingDaos.length} distinct)`);

const daos: DaoInfo[] = [];
const seenPools = new Set<string>();
let successCount = 0;
let failCount = 0;
let duplicateCount = allDaos.length - existingDaos.length;

for (let i = 0; i < existingDaos.length; i++) {
const address = existingDaos[i];
if (seenPools.has(address)) {
duplicateCount++;
continue;
}
seenPools.add(address);

console.log(`[${i + 1}/${existingDaos.length}] Fetching ${address.slice(0, 8)}...`);

const info = await fetchDaoInfo(address);
if (info) {
daos.push(info);
successCount++;
console.log(` -> ${info.baseSymbol}/${info.quoteSymbol} (${info.state})`);
await saveToken(String(info.baseMint));
await saveToken(String(info.quoteMint));
} else {
failCount++;
}

// Rate limit delay
if (i < existingDaos.length - 1) {
await sleep(DELAY_MS);
}
}

// Sort by baseSymbol
daos.sort((a, b) => String(a.baseSymbol || '').localeCompare(String(b.baseSymbol || '')));

// Save to file
const outputPath = './src/connectors/metadao/daos.json';
const fs = await import('fs');

fs.writeFileSync(outputPath, JSON.stringify(daos, null, 2));

console.log('\n=== Summary ===');
console.log(`Success: ${successCount}`);
console.log(`Failed: ${failCount}`);
console.log(`Skipped duplicates: ${duplicateCount}`);
console.log(`Distinct pools saved: ${daos.length}`);
console.log(`Saved to: ${outputPath}`);
}

main().catch(console.error);
8 changes: 8 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { configRoutes } from './config/config.routes';
import { register0xRoutes } from './connectors/0x/0x.routes';
import { dflowRoutes } from './connectors/dflow/dflow.routes';
import { jupiterRoutes } from './connectors/jupiter/jupiter.routes';
import { metadaoRoutes } from './connectors/metadao/metadao.routes';
import { meteoraRoutes } from './connectors/meteora/meteora.routes';
import { okxRoutes } from './connectors/okx/okx.routes';
import { orcaRoutes } from './connectors/orca/orca.routes';
Expand Down Expand Up @@ -97,6 +98,10 @@ const swaggerOptions = {
name: '/connector/jupiter',
description: 'Jupiter connector endpoints',
},
{
name: '/connector/metadao',
description: 'MetaDAO Futarchy connector endpoints (experimental)',
},
{
name: '/connector/meteora',
description: 'Meteora connector endpoints',
Expand Down Expand Up @@ -312,6 +317,9 @@ const configureGatewayServer = () => {
prefix: '/connectors/dflow/router',
});

// MetaDAO routes (experimental)
app.register(metadaoRoutes.futarchy, { prefix: '/connectors/metadao/futarchy' });

// OKX DEX aggregator routes
app.register(okxRoutes.router, {
prefix: '/connectors/okx/router',
Expand Down
7 changes: 7 additions & 0 deletions src/config/routes/getConnectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PancakeswapConfig } from '#src/connectors/pancakeswap/pancakeswap.confi
import { ZeroXConfig } from '../../connectors/0x/0x.config';
import { DFlowConfig } from '../../connectors/dflow/dflow.config';
import { JupiterConfig } from '../../connectors/jupiter/jupiter.config';
import { MetaDaoConfig } from '../../connectors/metadao/metadao.config';
import { MeteoraConfig } from '../../connectors/meteora/meteora.config';
import { OkxConfig } from '../../connectors/okx/okx.config';
import { OrcaConfig } from '../../connectors/orca/orca.config';
Expand Down Expand Up @@ -86,6 +87,12 @@ export const connectorsConfig = [
chain: DFlowConfig.chain,
networks: [...DFlowConfig.networks],
},
{
name: 'metadao',
trading_types: [...MetaDaoConfig.tradingTypes],
chain: MetaDaoConfig.chain,
networks: [...MetaDaoConfig.networks],
},
{
name: 'okx',
trading_types: [...OkxConfig.tradingTypes],
Expand Down
88 changes: 88 additions & 0 deletions src/connectors/metadao/dao-lookup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* DAO lookup helper - finds DAO address by baseToken symbol or mint
*/

import * as fs from 'fs';
import * as path from 'path';

interface DaoEntry {
pool: string;
baseMint: string;
quoteMint: string;
baseSymbol: string;
quoteSymbol: string;
state: string;
totalLiquidity: number;
}

let daosCache: DaoEntry[] | null = null;

function loadDaos(): DaoEntry[] {
if (daosCache) return daosCache;

const daosPath = path.join(__dirname, 'daos.json');
const data = fs.readFileSync(daosPath, 'utf-8');
daosCache = JSON.parse(data) as DaoEntry[];
return daosCache;
}

/**
* Find DAO address by baseToken and optionally quoteToken
* Returns the DAO with highest liquidity when multiple DAOs match
* @param baseToken - Token symbol (e.g., "SOLO") or mint address
* @param quoteToken - Optional quote token symbol (e.g., "USDC") or mint address
* @returns DAO address or null if not found
*/
export function findDaoByBaseToken(baseToken: string, quoteToken?: string): string | null {
const daos = loadDaos();

// Filter function for quote token matching
const matchesQuote = (d: DaoEntry): boolean => {
if (!quoteToken) return true;
return d.quoteSymbol.toLowerCase() === quoteToken.toLowerCase() || d.quoteMint === quoteToken;
};

// First try exact base mint address match (most specific)
const byMint = daos.filter((d) => d.baseMint === baseToken && matchesQuote(d));
if (byMint.length > 0) {
return byMint.sort((a, b) => b.totalLiquidity - a.totalLiquidity)[0].pool;
}

// Then try symbol match - return DAO with highest liquidity if multiple matches
const bySymbol = daos
.filter((d) => d.baseSymbol.toLowerCase() === baseToken.toLowerCase() && matchesQuote(d))
.sort((a, b) => b.totalLiquidity - a.totalLiquidity);

if (bySymbol.length > 0) return bySymbol[0].pool;

return null;
}

/**
* Find all DAOs matching a baseToken (for cases where multiple DAOs have same symbol)
* @param baseToken - Token symbol or mint address
* @returns Array of matching DAO entries
*/
export function findAllDaosByBaseToken(baseToken: string): DaoEntry[] {
const daos = loadDaos();

// Match by symbol (case-insensitive) or mint address
return daos.filter((d) => d.baseSymbol.toLowerCase() === baseToken.toLowerCase() || d.baseMint === baseToken);
}

/**
* Get DAO entry by address
* @param daoAddress - DAO public key
* @returns DAO entry or null if not found
*/
export function getDaoEntry(daoAddress: string): DaoEntry | null {
const daos = loadDaos();
return daos.find((d) => d.pool === daoAddress) || null;
}

/**
* Clear the cache (useful for testing)
*/
export function clearDaoCache(): void {
daosCache = null;
}
Loading
Loading