diff --git a/scripts/create_pool_from_prices.js b/scripts/create_pool_from_prices.js new file mode 100644 index 0000000..5768470 --- /dev/null +++ b/scripts/create_pool_from_prices.js @@ -0,0 +1,405 @@ +#!/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'; + +// ============================================================================ +// 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; + +// Minimal ABIs needed for this script +const IPartyPlannerABI = [ + { + "type": "function", + "name": "newPool", + "inputs": [ + { "name": "name", "type": "string", "internalType": "string" }, + { "name": "symbol", "type": "string", "internalType": "string" }, + { "name": "tokens", "type": "address[]", "internalType": "contract IERC20[]" }, + { "name": "kappa", "type": "int128", "internalType": "int128" }, + { "name": "swapFeePpm", "type": "uint256", "internalType": "uint256" }, + { "name": "flashFeePpm", "type": "uint256", "internalType": "uint256" }, + { "name": "stable", "type": "bool", "internalType": "bool" }, + { "name": "payer", "type": "address", "internalType": "address" }, + { "name": "receiver", "type": "address", "internalType": "address" }, + { "name": "initialDeposits", "type": "uint256[]", "internalType": "uint256[]" }, + { "name": "initialLpAmount", "type": "uint256", "internalType": "uint256" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [ + { "name": "pool", "type": "address", "internalType": "contract IPartyPool" }, + { "name": "lpAmount", "type": "uint256", "internalType": "uint256" } + ], + "stateMutability": "nonpayable" + } +]; + +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" }] } +]; + +// ============================================================================ +// 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; + +// ============================================================================ +// 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); +}); \ No newline at end of file diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..bc80f23 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,13 @@ +{ + "name": "liquidity-party-scripts", + "version": "1.0.0", + "description": "Standalone scripts for Liquidity Party DEX development and testing", + "type": "module", + "private": true, + "scripts": { + "create-pool": "node create_pool_from_prices.js" + }, + "dependencies": { + "ethers": "^5.7.2" + } +}