/** * Drift Market Discovery Script * * Queries the Drift SDK to list all available perpetual markets with their * complete specifications including market index, oracle address, and order sizes. * * Usage: * npm run discover-markets * npx tsx scripts/discover-drift-markets.ts * npx tsx scripts/discover-drift-markets.ts FARTCOIN # Search for specific symbol */ import { initializeDriftService, getDriftService } from '../lib/drift/client' // Helper to decode market name from bytes (Drift stores names as 32-byte arrays) function decodeMarketName(nameBytes: number[] | Uint8Array | undefined): string { if (!nameBytes) return '' // Convert to string and trim null bytes const bytes = Array.isArray(nameBytes) ? nameBytes : Array.from(nameBytes) const name = String.fromCharCode(...bytes.filter(b => b !== 0)) return name.trim() } /** * Extract numeric value from Anchor-style enum objects * * Drift SDK uses Anchor which represents enums as objects like: * - { pyth: {} } for OracleSource.Pyth * - { active: {} } for MarketStatus.Active * * This helper safely extracts the key name and maps it to a numeric index, * or handles direct numeric values for backwards compatibility. * * @param enumValue - The enum value (object or number) * @param keyMap - Map of enum key names to their string representations * @returns The string representation of the enum value */ function getEnumName>( enumValue: unknown, keyMap: T ): string { // Handle null/undefined if (enumValue === null || enumValue === undefined) { return 'Unknown' } // Handle direct numeric values (legacy support) if (typeof enumValue === 'number') { const values = Object.values(keyMap) return values[enumValue] || `Unknown(${enumValue})` } // Handle Anchor-style enum objects like { pyth: {} } or { active: {} } if (typeof enumValue === 'object') { const keys = Object.keys(enumValue as object) if (keys.length === 1) { const key = keys[0].toLowerCase() // Find matching key in keyMap (case-insensitive) for (const [mapKey, mapValue] of Object.entries(keyMap)) { if (mapKey.toLowerCase() === key) { return mapValue } } // Return the key name capitalized if no map match return keys[0].charAt(0).toUpperCase() + keys[0].slice(1) } } return `Unknown(${JSON.stringify(enumValue)})` } // Oracle source name mappings (Anchor enum key to display name) const ORACLE_SOURCE_MAP: Record = { pyth: 'Pyth', switchboard: 'Switchboard', quoteAsset: 'QuoteAsset', pyth1K: 'Pyth1K', pyth1M: 'Pyth1M', pythStableCoin: 'PythStableCoin', prelaunch: 'Prelaunch', pythPull: 'PythPull', pyth1KPull: 'Pyth1KPull', pyth1MPull: 'Pyth1MPull', pythStableCoinPull: 'PythStableCoinPull', switchboardOnDemand: 'SwitchboardOnDemand', } // Market status name mappings const MARKET_STATUS_MAP: Record = { initialized: 'Initialized', active: 'Active', fundingPaused: 'FundingPaused', ammPaused: 'AmmPaused', fillPaused: 'FillPaused', withdrawPaused: 'WithdrawPaused', reduceOnly: 'ReduceOnly', settlement: 'Settlement', delisted: 'Delisted', } // Contract type name mappings const CONTRACT_TYPE_MAP: Record = { perpetual: 'Perpetual', future: 'Future', } // Format number with appropriate decimals function formatNumber(value: number, decimals: number = 6): string { if (value === 0) return '0' if (value < 0.0001) return value.toExponential(2) return value.toFixed(decimals).replace(/\.?0+$/, '') } interface MarketInfo { index: number symbol: string oracleSource: string minOrderSize: number orderStepSize: number tickSize: number oracleAddress: string contractType: string imfFactor: number marginRatioInitial: number marginRatioMaintenance: number status: string } async function discoverMarkets(searchSymbol?: string): Promise { console.log('\nšŸ” Discovering Drift Protocol Markets...\n') try { // Initialize Drift service await initializeDriftService() const driftService = getDriftService() const driftClient = driftService.getClient() console.log('āœ… Connected to Drift Protocol\n') // Get all perp market accounts const perpMarketAccounts = driftClient.getPerpMarketAccounts() if (!perpMarketAccounts || perpMarketAccounts.length === 0) { console.log('āŒ No perpetual markets found') return } console.log(`šŸ“Š Found ${perpMarketAccounts.length} Perpetual Markets:\n`) // Collect market information const markets: MarketInfo[] = [] let foundSearchSymbol: MarketInfo | null = null for (let i = 0; i < perpMarketAccounts.length; i++) { const market = perpMarketAccounts[i] if (!market) continue // Extract market name from the name field (stored as bytes) let symbol = decodeMarketName(market.name as unknown as number[]) if (!symbol) { symbol = `Market-${i}` } // Get oracle information const oracleAddress = market.amm?.oracle?.toString() || 'N/A' const oracleSource = market.amm?.oracleSource // Get order sizing (values are stored with different precision) // Base asset precision is typically 9 decimals const minOrderSize = market.amm?.minOrderSize ? Number(market.amm.minOrderSize) / 1e9 : 0 const orderStepSize = market.amm?.orderStepSize ? Number(market.amm.orderStepSize) / 1e9 : 0 // Tick size for price (quote asset precision is 6 decimals for USDC) const tickSize = market.amm?.orderTickSize ? Number(market.amm.orderTickSize) / 1e6 : 0.0001 // Get margin requirements (stored as percentages with 4 decimal precision) const marginRatioInitial = market.marginRatioInitial ? Number(market.marginRatioInitial) / 10000 : 0 const marginRatioMaintenance = market.marginRatioMaintenance ? Number(market.marginRatioMaintenance) / 10000 : 0 // IMF factor (insurance margin fraction) const imfFactor = market.imfFactor ? Number(market.imfFactor) / 1e6 : 0 // Market status - use robust enum extraction const status = getEnumName(market.status, MARKET_STATUS_MAP) // Contract type - use robust enum extraction const contractType = getEnumName(market.contractType, CONTRACT_TYPE_MAP) const marketInfo: MarketInfo = { index: i, symbol, oracleSource: getEnumName(oracleSource, ORACLE_SOURCE_MAP), minOrderSize, orderStepSize, tickSize, oracleAddress, contractType, imfFactor, marginRatioInitial, marginRatioMaintenance, status, } markets.push(marketInfo) // Check if this matches our search if (searchSymbol && symbol.toUpperCase().includes(searchSymbol.toUpperCase())) { foundSearchSymbol = marketInfo } } // Print table header console.log('Index | Symbol | Oracle | Min Order | Step Size | Tick Size | Status | Oracle Address') console.log('------|----------------------|-------------|------------|------------|------------|-----------|' + '-'.repeat(50)) // Print each market for (const market of markets) { const isHighlighted = searchSymbol && market.symbol.toUpperCase().includes(searchSymbol.toUpperCase()) const highlight = isHighlighted ? ' ← FOUND!' : '' const row = [ market.index.toString().padStart(5), market.symbol.padEnd(20), market.oracleSource.padEnd(11), formatNumber(market.minOrderSize).padStart(10), formatNumber(market.orderStepSize).padStart(10), formatNumber(market.tickSize).padStart(10), market.status.padEnd(9), market.oracleAddress.substring(0, 44) + '...' + highlight, ].join(' | ') if (isHighlighted) { console.log(`\x1b[32m${row}\x1b[0m`) // Green highlight } else { console.log(row) } } // If searching for a specific symbol if (searchSymbol) { console.log('\n' + '='.repeat(100)) if (foundSearchSymbol) { console.log(`\nšŸŽÆ ${foundSearchSymbol.symbol} Configuration:\n`) // Generate copy-paste config for config/trading.ts console.log('Copy this into config/trading.ts (SUPPORTED_MARKETS):') console.log('```typescript') console.log(`'${foundSearchSymbol.symbol}': {`) console.log(` symbol: '${foundSearchSymbol.symbol}',`) console.log(` driftMarketIndex: ${foundSearchSymbol.index},`) console.log(` pythPriceFeedId: '${foundSearchSymbol.oracleAddress}',`) console.log(` minOrderSize: ${foundSearchSymbol.minOrderSize},`) console.log(` tickSize: ${foundSearchSymbol.tickSize},`) console.log(` positionSize: 50, // Customize as needed`) console.log(` leverage: 15, // Customize as needed`) console.log(`},`) console.log('```') // Generate symbol normalizer snippet const symbolBase = foundSearchSymbol.symbol.replace('-PERP', '').replace('-SPOT', '') console.log('\nAlso add to normalizeTradingViewSymbol():') console.log('```typescript') console.log(`if (upper.includes('${symbolBase}')) return '${foundSearchSymbol.symbol}'`) console.log('```') // Print detailed market info console.log('\nšŸ“‹ Full Market Details:') console.log(' Market Index:', foundSearchSymbol.index) console.log(' Symbol:', foundSearchSymbol.symbol) console.log(' Contract Type:', foundSearchSymbol.contractType) console.log(' Status:', foundSearchSymbol.status) console.log(' Oracle Source:', foundSearchSymbol.oracleSource) console.log(' Oracle Address:', foundSearchSymbol.oracleAddress) console.log(' Min Order Size:', foundSearchSymbol.minOrderSize, 'units') console.log(' Order Step Size:', foundSearchSymbol.orderStepSize, 'units') console.log(' Tick Size (Price):', foundSearchSymbol.tickSize, 'USD') console.log(' Initial Margin Ratio:', (foundSearchSymbol.marginRatioInitial * 100).toFixed(2) + '%') console.log(' Maintenance Margin Ratio:', (foundSearchSymbol.marginRatioMaintenance * 100).toFixed(2) + '%') console.log(' IMF Factor:', foundSearchSymbol.imfFactor) // Calculate max leverage from margin requirement if (foundSearchSymbol.marginRatioInitial > 0) { const maxLeverage = Math.floor(1 / foundSearchSymbol.marginRatioInitial) console.log(' Max Leverage (calculated):', maxLeverage + 'x') } } else { console.log(`\nāš ļø ${searchSymbol.toUpperCase()} not found in available markets.`) console.log('\nPossible reasons:') console.log(' 1. The market may not exist on Drift yet') console.log(' 2. The symbol name might be different (try variations)') console.log(' 3. The market might be delisted or not yet active') console.log('\nTip: Check Drift app manually: https://app.drift.trade') // Suggest similar symbols const similar = markets.filter(m => m.symbol.toUpperCase().includes(searchSymbol.substring(0, 3).toUpperCase()) ) if (similar.length > 0) { console.log('\nSimilar symbols found:') similar.forEach(m => console.log(` - ${m.symbol} (index: ${m.index})`)) } } } // Summary console.log('\n' + '='.repeat(100)) console.log(`\nšŸ“Š Summary: ${markets.length} perpetual markets available`) const activeMarkets = markets.filter(m => m.status === 'Active') console.log(` Active markets: ${activeMarkets.length}`) const pythMarkets = markets.filter(m => m.oracleSource.includes('Pyth')) console.log(` Pyth oracle markets: ${pythMarkets.length}`) // Cleanup await driftService.disconnect() console.log('\nāœ… Discovery complete\n') } catch (error) { console.error('āŒ Error discovering markets:', error) throw error } } // Main entry point const searchArg = process.argv[2] discoverMarkets(searchArg) .then(() => { process.exit(0) }) .catch((error) => { console.error('\nāŒ Discovery failed:', error) process.exit(1) })