395 lines
14 KiB
JavaScript
395 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';
|
|
|
|
// 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 <file> Pool config JSON file (default: pool-og.json)
|
|
--amount <usd> USD amount to distribute (overrides config, default: ${INPUT_USD_AMOUNT})
|
|
--name <name> Pool name (overrides config)
|
|
--symbol <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);
|
|
});
|