create_pool refactor; test pool deployed; etc

This commit is contained in:
2026-05-10 17:57:45 -04:00
parent d31f99d330
commit e1d2acea0d
13 changed files with 2359 additions and 444 deletions

View File

@@ -8,117 +8,60 @@ import { ethers } from 'ethers';
import { readFile } from 'fs/promises';
import { config } from 'dotenv';
// Load environment variables from .env-secret
config({ path: new URL('../.env-secret', import.meta.url).pathname });
// Load environment variables from .env
config({ path: new URL('../.env', import.meta.url).pathname });
// ============================================================================
// CONFIGURATION
// LOAD POOL CONFIG
// ============================================================================
// Default pool parameters
const POOL_NAME = 'Original Genesis of Liquidity Party';
const POOL_SYMBOL = 'OG.LP';
const KAPPA = ethers.BigNumber.from('184467440737095520');
const FLASH_FEE_PPM = 5;
const INPUT_USD_AMOUNT = 75; // Size of initial mint in USD
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
// ============================================================================
// Network-specific configuration
const NETWORK_CONFIG = {
mockchain: {
rpcUrl: 'http://localhost:8545',
privateKey: '0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a', // Dev wallet #4 (sender)
receiverPrivateKey: '0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356', // Receiver wallet
receiverAddress: '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955', // Receiver public key
rpcUrl: process.env.MOCKCHAIN_RPC_URL || 'http://localhost:8545',
chainId: '31337',
tokens: {
USDT: {
address: '0xbdEd0D2bf404bdcBa897a74E6657f1f12e5C6fb6', // USXD on mockchain
coingeckoId: 'tether',
decimals: 6,
feePpm: 400
},
USDC: {
address: '0x2910E325cf29dd912E3476B61ef12F49cb931096', // FUSD on mockchain
coingeckoId: 'usd-coin',
decimals: 6,
feePpm: 400
}
}
},
mainnet: {
rpcUrl: process.env.MAINNET_RPC_URL,
privateKey: process.env.PRIVATE_KEY,
receiverAddress: '0xd3b310bd32d782f89eea49cb79656bcaccde7213', // Same as payer for mainnet
chainId: '1',
tokens: {
USDT: {
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
coingeckoId: 'tether',
decimals: 6,
feePpm: 40 // 0.4 bps
},
USDC: {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
coingeckoId: 'usd-coin',
decimals: 6,
feePpm: 40 // 0.4 bps
},
WBTC: {
address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
coingeckoId: 'wrapped-bitcoin',
decimals: 8,
feePpm: 2_00 // 2 bps
},
WETH: {
address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
coingeckoId: 'weth',
decimals: 18,
feePpm: 2_50 // 2.5 bps
},
UNI: {
address: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984',
coingeckoId: 'uniswap',
decimals: 18,
feePpm: 9_50 // 9.5 bps
},
WSOL: {
address: '0xD31a59c85aE9D8edEFeC411D448f90841571b89c', // Wormhole Wrapped SOL
coingeckoId: 'solana',
decimals: 9,
feePpm: 9_50 // 9.5 bps
},
TRX: {
address: '0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5',
coingeckoId: 'tron',
decimals: 6,
feePpm: 9_50 // 9.5 bps
},
AAVE: {
address: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9',
coingeckoId: 'aave',
decimals: 18,
feePpm: 12_50 // 12.5 bps
},
PEPE: {
address: '0x6982508145454Ce325dDbE47a25d4ec3d2311933',
coingeckoId: 'pepe',
decimals: 18,
feePpm: 18_50 // 18.5 bps
},
SHIB: {
address: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE',
coingeckoId: 'shiba-inu',
decimals: 18,
feePpm: 18_50 // 18.5 bps
}
}
}
};
// Network flag: 'mockchain' or 'mainnet'
const NETWORK = process.env.NETWORK || 'mainnet';
// Get current network config
const currentConfig = NETWORK_CONFIG[NETWORK];
if (!currentConfig) {
console.error(`[!] Invalid NETWORK: ${NETWORK}. Must be 'mockchain' or 'mainnet'`);
@@ -126,10 +69,10 @@ if (!currentConfig) {
}
const RPC_URL = currentConfig.rpcUrl;
const PRIVATE_KEY = currentConfig.privateKey;
const RECEIVER_ADDRESS = currentConfig.receiverAddress || process.env.RECEIVER_ADDRESS;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const RECEIVER_PRIVATE_KEY = process.env.RECEIVER_PRIVATE_KEY;
const RECEIVER_ADDRESS = process.env.RECEIVER_ADDRESS;
// Validate required config for mainnet
if (NETWORK === 'mainnet' && (!RPC_URL || !PRIVATE_KEY)) {
console.error('[!] Missing required environment variables for mainnet');
console.error(' Required: MAINNET_RPC_URL, PRIVATE_KEY');
@@ -137,26 +80,23 @@ if (NETWORK === 'mainnet' && (!RPC_URL || !PRIVATE_KEY)) {
}
if (!RECEIVER_ADDRESS) {
console.error('[!] Missing RECEIVER_ADDRESS in .env-secret file');
console.error('[!] Missing RECEIVER_ADDRESS in .env file');
process.exit(1);
}
// Use network-specific tokens
const TEST_TOKENS = currentConfig.tokens;
const DEFAULT_POOL_PARAMS = {
name: POOL_NAME,
symbol: POOL_SYMBOL,
kappa: KAPPA, //0.01 * 2^64
name: poolConfig.name,
symbol: poolConfig.symbol,
kappa: KAPPA,
swapFeesPpm: Object.values(TEST_TOKENS).map(t => t.feePpm),
flashFeePpm: FLASH_FEE_PPM, // 0.0005%
stable: false,
flashFeePpm: FLASH_FEE_PPM,
stable: poolConfig.stable,
initialLpAmount: ethers.utils.parseUnits(INPUT_USD_AMOUNT.toString(), 18)
};
// ============================================================================
// LOAD ABIs AND CONFIG
// LOAD ABIs AND CONTRACT ADDRESSES
// ============================================================================
const chainInfoData = JSON.parse(await readFile(new URL('../src/contracts/liqp-deployments.json', import.meta.url), 'utf-8'));
@@ -165,6 +105,7 @@ const PARTY_PLANNER_ADDRESS = chainInfoData[currentConfig.chainId].v1.PartyPlann
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" }] }
];
@@ -173,9 +114,6 @@ const ERC20ABI = [
// HELPER FUNCTIONS
// ============================================================================
/**
* Fetch real-time prices from CoinGecko API
*/
async function fetchCoinGeckoPrices() {
try {
const ids = Object.values(TEST_TOKENS).map(t => t.coingeckoId).join(',');
@@ -211,24 +149,17 @@ async function fetchCoinGeckoPrices() {
}
}
/**
* 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 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)) {
// 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)`);
}
@@ -236,9 +167,6 @@ function calculateTokenAmounts(prices, usdAmount) {
return tokenAmounts;
}
/**
* Check token balances
*/
async function checkBalances(provider, wallet, tokenAmounts, receiverAddress) {
console.log(`\n[~] Checking token balances for receiver wallet: ${receiverAddress}`);
@@ -272,29 +200,32 @@ async function checkBalances(provider, wallet, tokenAmounts, receiverAddress) {
return balances;
}
/**
* Approve tokens for the PartyPlanner contract
*/
async function approveTokens(provider, tokenAmounts, receiverPrivateKey) {
async function approveTokens(provider, tokenAmounts, signerPrivateKey) {
console.log(`\n[~] Approving tokens for PartyPlanner contract...`);
// Connect with receiver wallet for approvals
const receiverWallet = new ethers.Wallet(receiverPrivateKey, provider);
console.log(`[~] Using receiver wallet for approvals: ${receiverWallet.address}`);
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, receiverWallet);
const tokenContract = new ethers.Contract(tokenInfo.address, ERC20ABI, signerWallet);
// 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} ${tokenInfo.address} ${approvalAmount} (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 == 'USDT') {
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`);
@@ -312,29 +243,24 @@ async function approveTokens(provider, tokenAmounts, receiverPrivateKey) {
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 5 minutes from now
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()}`);
// 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}" \
@@ -356,10 +282,14 @@ async function createPool(wallet, tokenAmounts) {
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' });
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);
@@ -373,22 +303,21 @@ async function createPool(wallet, tokenAmounts) {
}
}
/**
* 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}")
--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 --amount 200 --name "My Pool" --symbol "MP"
node create_pool_from_prices.js --config pool-test.json
node create_pool_from_prices.js --config pool-test.json --amount 200
`);
}
@@ -399,12 +328,9 @@ Example:
async function main() {
console.log(`${'='.repeat(70)}`);
console.log(`Create Pool from Real-Time Prices`);
console.log(`Network: ${NETWORK}`);
console.log(`Network: ${NETWORK} Config: ${configFile}`);
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);
@@ -427,37 +353,27 @@ async function main() {
}
}
// 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 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}`);
//
// Step 4: Check balances
await checkBalances(provider, wallet, tokenAmounts, RECEIVER_ADDRESS);
// // Step 5: Approve tokens
if (NETWORK === 'mockchain' && currentConfig.receiverPrivateKey) {
// On mockchain, use receiver wallet for approvals
await approveTokens(provider, tokenAmounts, currentConfig.receiverPrivateKey);
if (NETWORK === 'mockchain' && RECEIVER_PRIVATE_KEY) {
await approveTokens(provider, tokenAmounts, RECEIVER_PRIVATE_KEY);
} else if (NETWORK === 'mainnet') {
// On mainnet, use the main wallet (payer and receiver are the same)
await approveTokens(provider, tokenAmounts, PRIVATE_KEY);
}
// Step 6: Create pool
await createPool(wallet, tokenAmounts);
console.log(`\n${'='.repeat(70)}`);
@@ -472,8 +388,7 @@ async function main() {
}
}
// Run the main function
main().catch(error => {
console.error('[!] Unexpected error:', error);
process.exit(1);
});
});