adding script to create new pools based on realtime prices from coingeck
This commit is contained in:
405
scripts/create_pool_from_prices.js
Normal file
405
scripts/create_pool_from_prices.js
Normal file
@@ -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> 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 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);
|
||||
});
|
||||
13
scripts/package.json
Normal file
13
scripts/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user