#!/usr/bin/env node /** * Create Pool from Real-Time Prices Script * Fetches real-time prices from CoinGecko and creates a new pool on Anvil */ import { ethers } from 'ethers'; import { readFile } from 'fs/promises'; import IPartyPlannerABI from "../src/contracts/IPartyPlannerABI.ts"; // ============================================================================ // CONFIGURATION // ============================================================================ const ANVIL_RPC_URL = 'http://127.0.0.1:8545'; // Hardcoded private key for Anvil testing (default Anvil account #4 - payer) const PRIVATE_KEY = '0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a'; // Receiver address (Anvil account #7) const RECEIVER_ADDRESS = '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955'; // Test token addresses (mapping to real coins) const TEST_TOKENS = { USDC: { address: '0xbdEd0D2bf404bdcBa897a74E6657f1f12e5C6fb6', // USXD (mock) = USDC (real) coingeckoId: 'usd-coin', decimals: 6 }, BTC: { address: '0x93C7a6D00849c44Ef3E92E95DCEFfccd447909Ae', // BUTC (mock) = BTC (real) coingeckoId: 'bitcoin', decimals: 8 }, WETH: { address: '0x71a9d115E322467147391c4a71D85F8e1cA623EF', // WTETH (mock) = WETH (real) coingeckoId: 'weth', decimals: 18 } }; // Default pool parameters const DEFAULT_POOL_PARAMS = { name: 'Balanced Portfolio Pool', symbol: 'BPP', kappa: ethers.BigNumber.from('100000000000000000'), // 0.1 * 1e18 = 1e17 swapFeePpm: 3000, // 0.3% flashFeePpm: 5, // 0.0005% stable: false, initialLpAmount: ethers.BigNumber.from('1000000000000000000') // 1e18 }; // Input amount in USD const INPUT_USD_AMOUNT = 100; // ============================================================================ // LOAD ABIs AND CONFIG // ============================================================================ // Load chain info and get PartyPlanner address (chain ID 31337 = Anvil) const chainInfoData = JSON.parse(await readFile(new URL('../src/contracts/liqp-deployments.json', import.meta.url), 'utf-8')); const PARTY_PLANNER_ADDRESS = chainInfoData['31337'].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": "approve", "stateMutability": "nonpayable", "inputs": [{ "name": "spender", "type": "address" }, { "name": "amount", "type": "uint256" }], "outputs": [{ "name": "", "type": "bool" }] } ]; // ============================================================================ // HELPER FUNCTIONS // ============================================================================ /** * Fetch real-time prices from CoinGecko API */ 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 = { USDC: data['usd-coin']?.usd || 1, BTC: data['bitcoin']?.usd || 0, WETH: data['weth']?.usd || 0 }; if (prices.BTC === 0 || prices.WETH === 0) { throw new Error('Failed to fetch valid prices from CoinGecko'); } console.log(`[+] Prices fetched successfully:`); console.log(` USDC: $${prices.USDC}`); console.log(` BTC: $${prices.BTC.toLocaleString()}`); console.log(` WETH: $${prices.WETH.toLocaleString()}`); return prices; } catch (error) { console.error(`[!] Error fetching prices:`, error.message); throw error; } } /** * Calculate token amounts based on equal USD distribution */ function calculateTokenAmounts(prices, usdAmount) { const usdPerToken = usdAmount / 3; // Equally distribute among 3 tokens // Calculate raw amounts const usdcAmount = usdPerToken / prices.USDC; const btcAmount = usdPerToken / prices.BTC; const wethAmount = usdPerToken / prices.WETH; // Convert to BigNumber with proper decimals const usdcAmountBN = ethers.utils.parseUnits(usdcAmount.toFixed(TEST_TOKENS.USDC.decimals), TEST_TOKENS.USDC.decimals); const btcAmountBN = ethers.utils.parseUnits(btcAmount.toFixed(TEST_TOKENS.BTC.decimals), TEST_TOKENS.BTC.decimals); const wethAmountBN = ethers.utils.parseUnits(wethAmount.toFixed(TEST_TOKENS.WETH.decimals), TEST_TOKENS.WETH.decimals); console.log(`\n[~] Calculated token amounts for $${usdAmount} ($${usdPerToken.toFixed(2)} per token):`); console.log(` USDC: ${usdcAmount.toFixed(6)} USDC (${usdcAmountBN.toString()} wei)`); console.log(` BTC: ${btcAmount.toFixed(8)} BTC (${btcAmountBN.toString()} wei)`); console.log(` WETH: ${wethAmount.toFixed(8)} WETH (${wethAmountBN.toString()} wei)`); return { USDC: usdcAmountBN, BTC: btcAmountBN, WETH: wethAmountBN }; } /** * Check token balances */ async function checkBalances(provider, wallet, tokenAmounts) { console.log(`\n[~] Checking token balances for wallet: ${wallet.address}`); 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(wallet.address); 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 your wallet has enough tokens.`); throw new Error('Insufficient token balance'); } console.log(`[+] All balances sufficient`); return balances; } /** * Approve tokens for the PartyPlanner contract */ async function approveTokens(wallet, tokenAmounts) { console.log(`\n[~] Approving tokens for PartyPlanner contract...`); for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) { const tokenContract = new ethers.Contract(tokenInfo.address, ERC20ABI, wallet); // Approve 1% more than needed to account for fees/slippage const requiredAmount = tokenAmounts[symbol]; const approvalAmount = requiredAmount.mul(101).div(100); // 1% buffer console.log(` [~] Approving ${symbol} (1% buffer)...`); try { 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`); } /** * Create a new pool */ async function createPool(wallet, tokenAmounts) { console.log(`\n[~] Creating new pool...`); const partyPlanner = new ethers.Contract(PARTY_PLANNER_ADDRESS, IPartyPlannerABI, wallet); // Prepare parameters const tokenAddresses = [ TEST_TOKENS.USDC.address, TEST_TOKENS.BTC.address, TEST_TOKENS.WETH.address ]; const initialDeposits = [ tokenAmounts.USDC, tokenAmounts.BTC, tokenAmounts.WETH ]; // Set deadline to 1 hour from now const deadline = Math.floor(Date.now() / 1000) + 3600; console.log(`[~] Pool parameters:`); console.log(` Name: ${DEFAULT_POOL_PARAMS.name}`); console.log(` Symbol: ${DEFAULT_POOL_PARAMS.symbol}`); console.log(` Tokens: ${tokenAddresses.join(', ')}`); console.log(` Payer: ${wallet.address}`); console.log(` Receiver: ${RECEIVER_ADDRESS}`); console.log(` Deadline: ${new Date(deadline * 1000).toISOString()}`); try { const tx = await partyPlanner.newPool( DEFAULT_POOL_PARAMS.name, DEFAULT_POOL_PARAMS.symbol, tokenAddresses, DEFAULT_POOL_PARAMS.kappa, DEFAULT_POOL_PARAMS.swapFeePpm, DEFAULT_POOL_PARAMS.flashFeePpm, DEFAULT_POOL_PARAMS.stable, wallet.address, // payer (account #4) RECEIVER_ADDRESS, // receiver (account #7) initialDeposits, DEFAULT_POOL_PARAMS.initialLpAmount, deadline, ); console.log(`[~] Transaction submitted: ${tx.hash}`); console.log(`[~] Waiting for confirmation...`); const receipt = await tx.wait(); console.log(`[+] Pool created successfully!`); console.log(` Transaction: ${receipt.transactionHash}`); console.log(` Block: ${receipt.blockNumber}`); console.log(` Gas used: ${receipt.gasUsed.toString()}`); // Try to extract pool address from events if (receipt.events && receipt.events.length > 0) { const partyStartedEvent = receipt.events.find(e => e.event === 'PartyStarted'); if (partyStartedEvent && partyStartedEvent.args) { console.log(` Pool address: ${partyStartedEvent.args.pool}`); } } return receipt; } catch (error) { console.error(`[!] Failed to create pool:`, error.message); // Try to extract revert reason if available if (error.error && error.error.message) { console.error(` Revert reason: ${error.error.message}`); } throw error; } } /** * Print help message */ function printHelp() { console.log(` Usage: node create_pool_from_prices.js [OPTIONS] Options: --amount USD amount to distribute (default: ${INPUT_USD_AMOUNT}) --name Pool name (default: "${DEFAULT_POOL_PARAMS.name}") --symbol Pool symbol (default: "${DEFAULT_POOL_PARAMS.symbol}") --help, -h Show this help message Example: node create_pool_from_prices.js node create_pool_from_prices.js --amount 200 --name "My Pool" --symbol "MP" `); } // ============================================================================ // MAIN FUNCTION // ============================================================================ async function main() { console.log(`${'='.repeat(70)}`); console.log(`Create Pool from Real-Time Prices`); console.log(`${'='.repeat(70)}\n`); // Parse command line arguments const args = process.argv.slice(2); 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++; } } // Update pool params with parsed values DEFAULT_POOL_PARAMS.name = poolName; DEFAULT_POOL_PARAMS.symbol = poolSymbol; try { // Step 1: Fetch prices const prices = await fetchCoinGeckoPrices(); // Step 2: Calculate token amounts const tokenAmounts = calculateTokenAmounts(prices, usdAmount); // Step 3: Connect to Anvil console.log(`\n[~] Connecting to Anvil at ${ANVIL_RPC_URL}...`); const provider = new ethers.providers.JsonRpcProvider(ANVIL_RPC_URL); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); console.log(`[+] Connected. Using wallet: ${wallet.address}`); // Step 4: Check balances await checkBalances(provider, wallet, tokenAmounts); // Step 5: Approve tokens await approveTokens(wallet, tokenAmounts); // Step 6: Create pool 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); } } // Run the main function main().catch(error => { console.error('[!] Unexpected error:', error); process.exit(1); });