Files
web/scripts/create_pool_from_prices.js

418 lines
14 KiB
JavaScript

#!/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';
import IPartyPlannerABI from "../src/contracts/IPartyPlannerABI.ts";
// Load environment variables from .env-secret
config({ path: new URL('../.env-secret', import.meta.url).pathname });
// ============================================================================
// CONFIGURATION
// ============================================================================
const RPC_URL = process.env.MAINNET_RPC_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const RECEIVER_ADDRESS = process.env.RECEIVER_ADDRESS;
if (!RPC_URL || !PRIVATE_KEY || !RECEIVER_ADDRESS) {
console.error('[!] Missing required environment variables in .env-secret file');
console.error(' Required: MAINNET_RPC_URL, PRIVATE_KEY, RECEIVER_ADDRESS');
process.exit(1);
}
// Curated token list with real addresses (Ethereum Mainnet)
const TEST_TOKENS = {
USDT: {
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // Mainnet USDT
coingeckoId: 'tether',
decimals: 6,
feePpm: 400 // 0.00004 = 0.004% = 40 ppm
},
USDC: {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // Mainnet USDC
coingeckoId: 'usd-coin',
decimals: 6,
feePpm: 400 // 0.00004 = 0.004% = 40 ppm
},
WBTC: {
address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', // Mainnet WBTC
coingeckoId: 'wrapped-bitcoin',
decimals: 8,
feePpm: 3000 // 0.00030 = 0.03% = 300 ppm
},
WETH: {
address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // Mainnet WETH
coingeckoId: 'weth',
decimals: 18,
feePpm: 3500 // 0.00035 = 0.035% = 350 ppm
},
BNB: {
address: '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', // Mainnet BNB (Binance-Peg)
coingeckoId: 'binancecoin',
decimals: 18,
feePpm: 4500 // 0.00045 = 0.045% = 450 ppm
},
WSOL: {
address: '0xD31a59c85aE9D8edEFeC411D448f90841571b89c', // Mainnet Wormhole Wrapped SOL
coingeckoId: 'solana',
decimals: 9,
feePpm: 9500 // 0.00095 = 0.095% = 950 ppm
},
TRX: {
address: '0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5', // Mainnet TRX (Wrapped)
coingeckoId: 'tron',
decimals: 6,
feePpm: 9500 // 0.00095 = 0.095% = 950 ppm
},
AAVE: {
address: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', // Mainnet AAVE
coingeckoId: 'aave',
decimals: 18,
feePpm: 14500 // 0.00145 = 0.145% = 1450 ppm
},
PEPE: {
address: '0x6982508145454Ce325dDbE47a25d4ec3d2311933', // Mainnet PEPE
coingeckoId: 'pepe',
decimals: 18,
feePpm: 21500 // 0.00215 = 0.215% = 2150 ppm
},
SHIB: {
address: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE', // Mainnet SHIB
coingeckoId: 'shiba-inu',
decimals: 18,
feePpm: 21500 // 0.00215 = 0.215% = 2150 ppm
}
};
// Default pool parameters
const DEFAULT_POOL_PARAMS = {
name: 'Staging LP Pool',
symbol: 'SLP',
kappa: ethers.BigNumber.from('10000000000000000'), // 0.01 * 1e18
swapFeesPpm: Object.values(TEST_TOKENS).map(t => t.feePpm),
flashFeePpm: 5, // 0.0005%
stable: false,
initialLpAmount: ethers.utils.parseUnits('100', 18) // 100 USD in 18 decimals
};
// Input amount in USD
const INPUT_USD_AMOUNT = 1;
// ============================================================================
// LOAD ABIs AND CONFIG
// ============================================================================
const chainInfoData = JSON.parse(await readFile(new URL('../src/contracts/liqp-deployments.json', import.meta.url), 'utf-8'));
const PARTY_PLANNER_ADDRESS = chainInfoData['1'].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 = {};
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;
}
}
/**
* Calculate token amounts based on equal USD distribution
*/
function calculateTokenAmounts(prices, usdAmount) {
const tokenCount = Object.keys(TEST_TOKENS).length;
const usdPerToken = usdAmount / tokenCount; // Equally distribute among all tokens
const tokenAmounts = {};
console.log(`\n[~] Calculated token amounts for $${usdAmount} ($${usdPerToken.toFixed(2)} per token):`);
for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) {
// Calculate raw amount
const rawAmount = usdPerToken / prices[symbol];
// Convert to BigNumber with proper decimals
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;
}
/**
* 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 {
// USDT and some tokens require setting allowance to 0 before setting a new value
// Skip for BNB as it has a broken approve function
if (symbol !== 'BNB') {
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`);
}
/**
* Create a new pool using cast send
*/
async function createPool(wallet, tokenAmounts) {
console.log(`\n[~] Creating new pool...`);
// Prepare parameters
const tokenAddresses = Object.values(TEST_TOKENS).map(t => t.address);
const initialDeposits = Object.keys(TEST_TOKENS).map(symbol => tokenAmounts[symbol].toString());
// 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(` Swap Fees PPM: [${DEFAULT_POOL_PARAMS.swapFeesPpm.join(', ')}]`);
console.log(` Payer: ${wallet.address}`);
console.log(` Receiver: ${RECEIVER_ADDRESS}`);
console.log(` Deadline: ${new Date(deadline * 1000).toISOString()}`);
// Build cast send command
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} \
${wallet.address} \
${RECEIVER_ADDRESS} \
"[${initialDeposits.join(',')}]" \
${DEFAULT_POOL_PARAMS.initialLpAmount.toString()} \
${deadline} \
--rpc-url http://localhost:8545 \
--from 0x12db90820dafed100e40e21128e40dcd4ff6b331 \
--trezor --mnemonic-index 0`;
// --private-key ${PRIVATE_KEY};
console.log(`\n[~] Cast command:\n${castCommand}\n`);
try {
// Execute cast command
const { execSync } = await import('child_process');
const output = execSync(castCommand, { encoding: 'utf-8' });
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;
}
}
/**
* Print help message
*/
function printHelp() {
console.log(`
Usage: node create_pool_from_prices.js [OPTIONS]
Options:
--amount <usd> USD amount to distribute (default: ${INPUT_USD_AMOUNT})
--name <name> Pool name (default: "${DEFAULT_POOL_PARAMS.name}")
--symbol <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 wallet
console.log(`\n[~] Connecting to test wallet at ${RPC_URL}...`);
const provider = new ethers.providers.JsonRpcProvider(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);
});