#!/usr/bin/env node /** * Create Pool from Real-Time Prices Script * Fetches real-time prices from CoinGecko and creates a new pool on the chain specified (could be mockchain, sepolia or prod) */ import { ethers } from 'ethers'; import { readFile } from 'fs/promises'; import { config } from 'dotenv'; // Load environment variables from .env config({ path: new URL('../.env', import.meta.url).pathname }); // ============================================================================ // LOAD POOL CONFIG // ============================================================================ const args = process.argv.slice(2); function getArg(flag) { const idx = args.indexOf(flag); return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null; } const configFile = getArg('--config') || 'pool-test.json'; const poolConfig = JSON.parse( await readFile(new URL(configFile, import.meta.url), 'utf-8') ); const TEST_TOKENS = poolConfig.tokens; // Convert kappa from float to Q64.64 fixed-point BigInt // e.g. 0.01 -> round(0.01 * 2^64) function kappaToFixedPoint(kappaFloat) { const str = kappaFloat.toString(); const [whole, frac = ''] = str.split('.'); const precision = frac.length; const numerator = BigInt(whole + frac); const denominator = BigInt(10 ** precision); const scale = 2n ** 64n; return (numerator * scale + denominator / 2n) / denominator; } const KAPPA = ethers.BigNumber.from(kappaToFixedPoint(poolConfig.kappa).toString()); const FLASH_FEE_PPM = poolConfig.flashFeePpm; const INPUT_USD_AMOUNT = poolConfig.inputUsdAmount; // ============================================================================ // NETWORK CONFIGURATION // ============================================================================ const NETWORK_CONFIG = { mockchain: { rpcUrl: process.env.MOCKCHAIN_RPC_URL || 'http://localhost:8545', chainId: '31337', }, mainnet: { rpcUrl: process.env.MAINNET_RPC_URL, chainId: '1', } }; const NETWORK = process.env.NETWORK || 'mainnet'; const currentConfig = NETWORK_CONFIG[NETWORK]; if (!currentConfig) { console.error(`[!] Invalid NETWORK: ${NETWORK}. Must be 'mockchain' or 'mainnet'`); process.exit(1); } const RPC_URL = currentConfig.rpcUrl; const PRIVATE_KEY = process.env.PRIVATE_KEY; const RECEIVER_PRIVATE_KEY = process.env.RECEIVER_PRIVATE_KEY; const RECEIVER_ADDRESS = process.env.RECEIVER_ADDRESS; if (NETWORK === 'mainnet' && (!RPC_URL || !PRIVATE_KEY)) { console.error('[!] Missing required environment variables for mainnet'); console.error(' Required: MAINNET_RPC_URL, PRIVATE_KEY'); process.exit(1); } if (!RECEIVER_ADDRESS) { console.error('[!] Missing RECEIVER_ADDRESS in .env file'); process.exit(1); } const DEFAULT_POOL_PARAMS = { name: poolConfig.name, symbol: poolConfig.symbol, kappa: KAPPA, swapFeesPpm: Object.values(TEST_TOKENS).map(t => t.feePpm), flashFeePpm: FLASH_FEE_PPM, stable: poolConfig.stable, initialLpAmount: ethers.utils.parseUnits(INPUT_USD_AMOUNT.toString(), 18) }; // ============================================================================ // LOAD ABIs AND CONTRACT ADDRESSES // ============================================================================ const chainInfoData = JSON.parse(await readFile(new URL('../src/contracts/liqp-deployments.json', import.meta.url), 'utf-8')); const PARTY_PLANNER_ADDRESS = chainInfoData[currentConfig.chainId].v1.PartyPlanner; const ERC20ABI = [ { "type": "function", "name": "balanceOf", "stateMutability": "view", "inputs": [{ "name": "account", "type": "address" }], "outputs": [{ "name": "", "type": "uint256" }] }, { "type": "function", "name": "decimals", "stateMutability": "view", "inputs": [], "outputs": [{ "name": "", "type": "uint8" }] }, { "type": "function", "name": "allowance", "stateMutability": "view", "inputs": [{ "name": "owner", "type": "address" }, { "name": "spender", "type": "address" }], "outputs": [{ "name": "", "type": "uint256" }] }, { "type": "function", "name": "approve", "stateMutability": "nonpayable", "inputs": [{ "name": "spender", "type": "address" }, { "name": "amount", "type": "uint256" }], "outputs": [{ "name": "", "type": "bool" }] } ]; // ============================================================================ // HELPER FUNCTIONS // ============================================================================ async function fetchCoinGeckoPrices() { try { const ids = Object.values(TEST_TOKENS).map(t => t.coingeckoId).join(','); const url = `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`; console.log(`[~] Fetching prices from CoinGecko...`); const response = await fetch(url); if (!response.ok) { throw new Error(`CoinGecko API request failed: ${response.statusText}`); } const data = await response.json(); const prices = {}; for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) { prices[symbol] = data[tokenInfo.coingeckoId]?.usd || 0; if (prices[symbol] === 0) { throw new Error(`Failed to fetch valid price for ${symbol}`); } } console.log(`[+] Prices fetched successfully:`); for (const [symbol, price] of Object.entries(prices)) { console.log(` ${symbol.padEnd(6)}: $${price.toLocaleString()}`); } return prices; } catch (error) { console.error(`[!] Error fetching prices:`, error.message); throw error; } } function calculateTokenAmounts(prices, usdAmount) { const tokenCount = Object.keys(TEST_TOKENS).length; const usdPerToken = usdAmount / tokenCount; const tokenAmounts = {}; console.log(`\n[~] Calculated token amounts for $${usdAmount} ($${usdPerToken.toFixed(2)} per token):`); for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) { const rawAmount = usdPerToken / prices[symbol]; const amountBN = ethers.utils.parseUnits(rawAmount.toFixed(tokenInfo.decimals), tokenInfo.decimals); tokenAmounts[symbol] = amountBN; console.log(` ${symbol.padEnd(6)}: ${rawAmount.toFixed(tokenInfo.decimals)} (${amountBN.toString()} wei)`); } return tokenAmounts; } async function checkBalances(provider, wallet, tokenAmounts, receiverAddress) { console.log(`\n[~] Checking token balances for receiver wallet: ${receiverAddress}`); const balances = {}; let hasEnoughBalance = true; for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) { const tokenContract = new ethers.Contract(tokenInfo.address, ERC20ABI, provider); const balance = await tokenContract.balanceOf(receiverAddress); const requiredAmount = tokenAmounts[symbol]; balances[symbol] = balance; const balanceFormatted = ethers.utils.formatUnits(balance, tokenInfo.decimals); const requiredFormatted = ethers.utils.formatUnits(requiredAmount, tokenInfo.decimals); const sufficient = balance.gte(requiredAmount); console.log(` ${symbol}: ${balanceFormatted} (required: ${requiredFormatted}) ${sufficient ? '✓' : '✗'}`); if (!sufficient) { hasEnoughBalance = false; } } if (!hasEnoughBalance) { console.log(`\n[!] Insufficient token balance. Please ensure receiver wallet has enough tokens.`); throw new Error('Insufficient token balance'); } console.log(`[+] All balances sufficient`); return balances; } async function approveTokens(provider, tokenAmounts, signerPrivateKey) { console.log(`\n[~] Approving tokens for PartyPlanner contract...`); const signerWallet = new ethers.Wallet(signerPrivateKey, provider); console.log(`[~] Using wallet for approvals: ${signerWallet.address}`); for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) { const tokenContract = new ethers.Contract(tokenInfo.address, ERC20ABI, signerWallet); const requiredAmount = tokenAmounts[symbol]; const approvalAmount = requiredAmount.mul(101).div(100); // 1% buffer try { const currentAllowance = await tokenContract.allowance(signerWallet.address, PARTY_PLANNER_ADDRESS); const currentFormatted = ethers.utils.formatUnits(currentAllowance, tokenInfo.decimals); const requiredFormatted = ethers.utils.formatUnits(approvalAmount, tokenInfo.decimals); if (currentAllowance.gte(approvalAmount)) { console.log(` [=] ${symbol} allowance already sufficient: ${currentFormatted} >= ${requiredFormatted} (skipping)`); continue; } console.log(` [~] Approving ${symbol} ${tokenInfo.address} ${approvalAmount} (current: ${currentFormatted}, need: ${requiredFormatted})...`); // USDT and some tokens require setting allowance to 0 before raising it if (symbol == 'USDT' && !currentAllowance.isZero()) { const resetTx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, 0); await resetTx.wait(); console.log(` [+] ${symbol} allowance reset to 0`); } const tx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, approvalAmount); await tx.wait(); console.log(` [+] ${symbol} approved (tx: ${tx.hash})`); } catch (error) { console.error(` [!] Failed to approve ${symbol}:`, error.message); throw error; } } console.log(`[+] All tokens approved`); } async function createPool(wallet, tokenAmounts) { console.log(`\n[~] Creating new pool...`); const tokenAddresses = Object.values(TEST_TOKENS).map(t => t.address); const initialDeposits = Object.keys(TEST_TOKENS).map(symbol => tokenAmounts[symbol].toString()); const deadline = Math.floor(Date.now() / 1000) + 300; console.log(`[~] Pool parameters:`); console.log(` Name: ${DEFAULT_POOL_PARAMS.name}`); console.log(` Symbol: ${DEFAULT_POOL_PARAMS.symbol}`); console.log(` Kappa: ${poolConfig.kappa} (${KAPPA.toString()})`); console.log(` Tokens: ${tokenAddresses.join(', ')}`); console.log(` Swap Fees PPM: [${DEFAULT_POOL_PARAMS.swapFeesPpm.join(', ')}]`); console.log(` Payer (provides tokens): ${RECEIVER_ADDRESS}`); console.log(` Receiver (gets LP tokens): ${wallet.address}`); console.log(` Deadline: ${new Date(deadline * 1000).toISOString()}`); const castCommand = `cast send ${PARTY_PLANNER_ADDRESS} \ "newPool(string,string,address[],int128,uint256[],uint256,bool,address,address,uint256[],uint256,uint256)" \ "${DEFAULT_POOL_PARAMS.name}" \ "${DEFAULT_POOL_PARAMS.symbol}" \ "[${tokenAddresses.join(',')}]" \ ${DEFAULT_POOL_PARAMS.kappa.toString()} \ "[${DEFAULT_POOL_PARAMS.swapFeesPpm.join(',')}]" \ ${DEFAULT_POOL_PARAMS.flashFeePpm} \ ${DEFAULT_POOL_PARAMS.stable} \ ${RECEIVER_ADDRESS} \ ${wallet.address} \ "[${initialDeposits.join(',')}]" \ ${DEFAULT_POOL_PARAMS.initialLpAmount.toString()} \ ${deadline} \ --rpc-url '${RPC_URL}' \ --from 0x12db90820dafed100e40e21128e40dcd4ff6b331 \ --trezor --mnemonic-index 0` console.log(`\n[~] Cast command:\n${castCommand}\n`); try { const { execSync } = await import('child_process'); const foundryBin = `${process.env.HOME}/.foundry/bin`; const env = { ...process.env, PATH: `${foundryBin}:${process.env.PATH || ''}`, }; const output = execSync(castCommand, { encoding: 'utf-8', env }); console.log(`[+] Pool created successfully!`); console.log(output); return output; } catch (error) { console.error(`[!] Failed to create pool:`, error.message); if (error.stderr) { console.error(` Error output: ${error.stderr.toString()}`); } throw error; } } function printHelp() { console.log(` Usage: node create_pool_from_prices.js [OPTIONS] Options: --config Pool config JSON file (default: pool-og.json) --amount USD amount to distribute (overrides config, default: ${INPUT_USD_AMOUNT}) --name Pool name (overrides config) --symbol Pool symbol (overrides config) --help, -h Show this help message Example: node create_pool_from_prices.js node create_pool_from_prices.js --config pool-test.json node create_pool_from_prices.js --config pool-test.json --amount 200 `); } // ============================================================================ // MAIN FUNCTION // ============================================================================ async function main() { console.log(`${'='.repeat(70)}`); console.log(`Create Pool from Real-Time Prices`); console.log(`Network: ${NETWORK} Config: ${configFile}`); console.log(`${'='.repeat(70)}\n`); if (args.includes('--help') || args.includes('-h')) { printHelp(); process.exit(0); } let usdAmount = INPUT_USD_AMOUNT; let poolName = DEFAULT_POOL_PARAMS.name; let poolSymbol = DEFAULT_POOL_PARAMS.symbol; for (let i = 0; i < args.length; i++) { if (args[i] === '--amount' && i + 1 < args.length) { usdAmount = parseFloat(args[i + 1]); i++; } else if (args[i] === '--name' && i + 1 < args.length) { poolName = args[i + 1]; i++; } else if (args[i] === '--symbol' && i + 1 < args.length) { poolSymbol = args[i + 1]; i++; } } DEFAULT_POOL_PARAMS.name = poolName; DEFAULT_POOL_PARAMS.symbol = poolSymbol; try { const prices = await fetchCoinGeckoPrices(); const tokenAmounts = calculateTokenAmounts(prices, usdAmount); console.log(`\n[~] Connecting to sender wallet at ${RPC_URL}...`); const provider = new ethers.providers.JsonRpcProvider(RPC_URL); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); console.log(`[+] Connected. Using sender wallet: ${wallet.address}`); console.log(`[+] Receiver wallet: ${RECEIVER_ADDRESS}`); await checkBalances(provider, wallet, tokenAmounts, RECEIVER_ADDRESS); if (NETWORK === 'mockchain' && RECEIVER_PRIVATE_KEY) { await approveTokens(provider, tokenAmounts, RECEIVER_PRIVATE_KEY); } else if (NETWORK === 'mainnet') { await approveTokens(provider, tokenAmounts, PRIVATE_KEY); } await createPool(wallet, tokenAmounts); console.log(`\n${'='.repeat(70)}`); console.log(`Success! Pool created with real-time price-based deposits.`); console.log(`${'='.repeat(70)}\n`); } catch (error) { console.error(`\n${'='.repeat(70)}`); console.error(`[!] Error: ${error.message}`); console.error(`${'='.repeat(70)}\n`); process.exit(1); } } main().catch(error => { console.error('[!] Unexpected error:', error); process.exit(1); });