diff --git a/bin/deploy b/bin/deploy index 6650ead..92ac51f 100755 --- a/bin/deploy +++ b/bin/deploy @@ -26,6 +26,14 @@ else echo Building fi +# Load secret env vars (including NEXT_PUBLIC_* vars baked into the static bundle) +if [ -f .env-secret ]; then + set -a + # shellcheck disable=SC1091 + source .env-secret + set +a +fi + npm run build || exit 1 if [ "$BUILD" != "1" ]; then diff --git a/scripts/adjust_pool_prices_and_stake.js b/scripts/adjust_pool_prices_and_stake.js new file mode 100644 index 0000000..a0f93c8 --- /dev/null +++ b/scripts/adjust_pool_prices_and_stake.js @@ -0,0 +1,911 @@ +#!/usr/bin/env node +/** + * Adjust Pool Prices Script + * Rebalances an existing LiqP pool's prices to match market prices. + * Uses IPartyInfo.swapToLimitAmounts() to compute the required swap size, + * then executes via pool.swap() (swapToLimit has a known implementation bug). + * Runs two passes to compensate for LMSR price coupling across assets. + * Optionally basket-stakes a configurable USD amount via mint(). + */ + +import { ethers } from 'ethers'; +import { readFile } from 'fs/promises'; +import { config } from 'dotenv'; +import { execSync } from 'child_process'; + +config({ path: new URL('../.env', import.meta.url).pathname }); + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +const NETWORK_CONFIG = { + mockchain: { + rpcUrl: 'http://localhost:8545', + chainId: '31337', + tokens: { + USDT: { address: '0xbdEd0D2bf404bdcBa897a74E6657f1f12e5C6fb6', coingeckoId: 'tether', decimals: 6 }, + USDC: { address: '0x2910E325cf29dd912E3476B61ef12F49cb931096', coingeckoId: 'usd-coin', decimals: 6 } + } + }, + mainnet: { + rpcUrl: process.env.MAINNET_RPC_URL, + chainId: '1', + tokens: { + USDT: { address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', coingeckoId: 'tether', decimals: 6 }, + USDC: { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', coingeckoId: 'usd-coin', decimals: 6 }, + WBTC: { address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', coingeckoId: 'wrapped-bitcoin', decimals: 8 }, + WETH: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', coingeckoId: 'weth', decimals: 18 }, + UNI: { address: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', coingeckoId: 'uniswap', decimals: 18 }, + WSOL: { address: '0xD31a59c85aE9D8edEFeC411D448f90841571b89c', coingeckoId: 'solana', decimals: 9 }, + TRX: { address: '0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5', coingeckoId: 'tron', decimals: 6 }, + AAVE: { address: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', coingeckoId: 'aave', decimals: 18 }, + PEPE: { address: '0x6982508145454Ce325dDbE47a25d4ec3d2311933', coingeckoId: 'pepe', decimals: 18 }, + SHIB: { address: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE', coingeckoId: 'shiba-inu', decimals: 18 } + }, + uniswap: { + swapRouter02: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984', + // Only 0.05% (500) and 0.3% (3000) tiers are used — 1% pools are avoided. + defaultFeeTier: 3000, + feeTierOverrides: { USDT: 500, USDC: 500, WBTC: 3000, WETH: 500 } + } + } +}; + +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 TOKEN_CONFIG = currentConfig.tokens; + +// Price deviation threshold: only swap if price is off by more than this +const PRICE_DEVIATION_THRESHOLD = 0.001; // 0.1% + +// ============================================================================ +// ABIs (minimal subset needed) +// ============================================================================ + +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: 'symbol', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] }, + { type: 'function', name: 'approve', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }] }, + { type: 'function', name: 'allowance', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] } +]; + +const IPartyPoolABI = [ + { type: 'function', name: 'allTokens', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address[]' }] }, + { type: 'function', name: 'totalSupply', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256' }] }, + { type: 'function', name: 'balances', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256[]' }] }, + { type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] }, + { type: 'function', name: 'denominators', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256[]' }] } +]; + +const IPartyInfoABI = [ + { + type: 'function', name: 'price', stateMutability: 'view', + inputs: [{ name: 'pool', type: 'address' }, { name: 'baseTokenIndex', type: 'uint256' }, { name: 'quoteTokenIndex', type: 'uint256' }], + outputs: [{ name: '', type: 'uint256' }] + }, + { + type: 'function', name: 'swapToLimitAmounts', stateMutability: 'view', + inputs: [{ name: 'pool', type: 'address' }, { name: 'inputTokenIndex', type: 'uint256' }, { name: 'outputTokenIndex', type: 'uint256' }, { name: 'limitPrice', type: 'int128' }], + outputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'amountOut', type: 'uint256' }, { name: 'inFee', type: 'uint256' }] + }, + { + type: 'function', name: 'mintAmounts', stateMutability: 'view', + inputs: [{ name: 'pool', type: 'address' }, { name: 'lpTokenAmount', type: 'uint256' }], + outputs: [{ name: 'depositAmounts', type: 'uint256[]' }] + } +]; + +const UniswapV3FactoryABI = [ + { type: 'function', name: 'getPool', stateMutability: 'view', + inputs: [{ name: 'tokenA', type: 'address' }, { name: 'tokenB', type: 'address' }, { name: 'fee', type: 'uint24' }], + outputs: [{ name: 'pool', type: 'address' }] } +]; + +const UniswapV3PoolABI = [ + { type: 'function', name: 'liquidity', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint128' }] } +]; + +const UniswapSwapRouter02ABI = [ + { + type: 'function', name: 'exactOutputSingle', stateMutability: 'payable', + inputs: [{ + name: 'params', type: 'tuple', + components: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'fee', type: 'uint24' }, + { name: 'recipient', type: 'address' }, + { name: 'amountOut', type: 'uint256' }, + { name: 'amountInMaximum', type: 'uint256' }, + { name: 'sqrtPriceLimitX96', type: 'uint160' } + ] + }], + outputs: [{ name: 'amountIn', type: 'uint256' }] + } +]; + +// ============================================================================ +// SIGNER ABSTRACTION +// ============================================================================ + +/** + * Build a signer from parsed CLI options. + * + * mode 'privateKey': all writes go through ethers.Wallet (no cast required). + * mode 'trezor': all writes go through `cast send --trezor` (no private key stored). + */ +function buildSigner(opts, provider) { + if (opts.privateKey) { + const wallet = new ethers.Wallet(opts.privateKey, provider); + return { mode: 'privateKey', address: wallet.address, wallet, rpcUrl: currentConfig.rpcUrl }; + } else { + return { mode: 'trezor', address: opts.trezorAddress, wallet: null, rpcUrl: currentConfig.rpcUrl }; + } +} + +/** Execute a cast send command (Trezor mode). Throws on failure. */ +function runCast(signer, to, sigStr, argStr) { + const nonce = signer.nextNonce(); + const cmd = [ + `cast send ${to}`, + ` "${sigStr}"`, + ` ${argStr}`, + ` --rpc-url '${signer.rpcUrl}'`, + ` --from ${signer.address}`, + ` --nonce ${nonce}`, + ` --trezor --mnemonic-index 0` + ].join(' \\\n'); + console.log(` [cast] ${cmd.replace(/\s+/g, ' ')}`); + const output = execSync(cmd, { encoding: 'utf-8' }); + if (output) console.log(output.trim()); + return output; +} + +// ============================================================================ +// HELPERS +// ============================================================================ + +const Q64 = 2n ** 64n; + +/** + * Convert a floating-point value to Q64.64 BigInt. + * Uses 1e9-scaled integer arithmetic to avoid float precision loss. + */ +function floatToQ64(x) { + return BigInt(Math.round(x * 1e9)) * Q64 / 1_000_000_000n; +} + +/** + * Compute the limitPrice argument for swapToLimitAmounts. + * + * Derived from swapAmountsForPriceLimit: setting r0_new = r0_target in + * r0_new = r0² / (L×(1+r0) - r0²) and solving for L gives: + * + * L = r0i² × (1 + r0t) / (r0t × (1 + r0i)) + * + * Requires r0tQ64 < r0iQ64 (target price lower than current in swap direction). + * Both arguments are Q64.64 BigInts (raw internal LMSR price ratios, no denom scaling). + */ +function computeLimitPrice(r0iQ64, r0tQ64) { + return r0iQ64 * r0iQ64 * (r0tQ64 + Q64) / (r0tQ64 * (r0iQ64 + Q64)); +} + +async function fetchCoinGeckoPrices(symbols) { + const relevantTokens = Object.entries(TOKEN_CONFIG).filter(([sym]) => symbols.includes(sym)); + const ids = relevantTokens.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 error: ${response.statusText}`); + const data = await response.json(); + + const prices = {}; + for (const [symbol, tokenInfo] of relevantTokens) { + prices[symbol] = data[tokenInfo.coingeckoId]?.usd; + if (!prices[symbol]) throw new Error(`No price returned for ${symbol} (${tokenInfo.coingeckoId})`); + } + return prices; +} + +async function diagnoseGasError(err, signer, provider) { + if (err.code !== 'UNPREDICTABLE_GAS_LIMIT') return; + const [balance, feeData] = await Promise.all([ + provider.getBalance(signer.address), + provider.getFeeData(), + ]); + const maxFee = feeData.maxFeePerGas || feeData.gasPrice || ethers.BigNumber.from(0); + if (maxFee.isZero()) return; + const affordableGas = balance.div(maxFee).toNumber(); + if (affordableGas < 100_000) { + const needed = maxFee.mul(100_000).sub(balance); + throw new Error( + `Out of ETH for gas: wallet has ${ethers.utils.formatEther(balance)} ETH ` + + `(~${affordableGas.toLocaleString()} gas units at ${ethers.utils.formatUnits(maxFee, 'gwei')} gwei maxFee). ` + + `Top up at least ${ethers.utils.formatEther(needed)} ETH.` + ); + } +} + +async function approveToken(signer, provider, tokenAddress, tokenSymbol, spenderAddress, amount) { + const decimals = TOKEN_CONFIG[tokenSymbol]?.decimals ?? 18; + const tokenReadOnly = new ethers.Contract(tokenAddress, ERC20ABI, provider); + const currentAllowance = await tokenReadOnly.allowance(signer.address, spenderAddress); + + if (currentAllowance.gte(amount)) { + console.log(` [=] ${tokenSymbol} allowance already sufficient (${ethers.utils.formatUnits(currentAllowance, decimals)})`); + return; + } + + if (signer.mode === 'privateKey') { + try { + const token = new ethers.Contract(tokenAddress, ERC20ABI, signer.wallet); + if (tokenSymbol === 'USDT' && currentAllowance.gt(0)) { + const tx = await token.approve(spenderAddress, 0, { nonce: signer.nextNonce() }); + await tx.wait(); + console.log(` [+] ${tokenSymbol} allowance reset to 0`); + } + const tx = await token.approve(spenderAddress, amount, { nonce: signer.nextNonce() }); + await tx.wait(); + console.log(` [+] ${tokenSymbol} approved (tx: ${tx.hash})`); + } catch (err) { + await diagnoseGasError(err, signer, provider); + throw err; + } + } else { + if (tokenSymbol === 'USDT' && currentAllowance.gt(0)) { + runCast(signer, tokenAddress, 'approve(address,uint256)', `${spenderAddress} 0`); + console.log(` [+] ${tokenSymbol} allowance reset to 0`); + } + runCast(signer, tokenAddress, 'approve(address,uint256)', `${spenderAddress} ${amount.toString()}`); + console.log(` [+] ${tokenSymbol} approved`); + } +} + +async function checkPoolLiquidity(provider, tokenA, tokenB, feeTier) { + const uniswapConfig = currentConfig.uniswap; + const factory = new ethers.Contract(uniswapConfig.factory, UniswapV3FactoryABI, provider); + const poolAddress = await factory.getPool(tokenA, tokenB, feeTier); + if (poolAddress === ethers.constants.AddressZero) { + throw new Error( + `No Uniswap V3 pool exists for this pair at fee tier ${feeTier / 10000}% — ` + + `try a different fee tier or route through an intermediate token` + ); + } + const pool = new ethers.Contract(poolAddress, UniswapV3PoolABI, provider); + const liquidity = await pool.liquidity(); + if (liquidity.isZero()) { + throw new Error( + `Uniswap V3 pool ${poolAddress} has no active in-range liquidity at fee tier ${feeTier / 10000}% — ` + + `the pool exists but all liquidity is out of range` + ); + } +} + +async function buyFromUniswap(signer, provider, tokenOut, tokenOutSymbol, amountOut, tokenIn, tokenInSymbol, maxAmountIn, dryRun) { + const uniswapConfig = currentConfig.uniswap; + if (!uniswapConfig) { + throw new Error(`Uniswap not configured for network '${NETWORK}' — cannot buy ${tokenOutSymbol}`); + } + + const feeTier = uniswapConfig.feeTierOverrides[tokenOutSymbol] ?? uniswapConfig.defaultFeeTier; + const outDecimals = TOKEN_CONFIG[tokenOutSymbol]?.decimals ?? 18; + const inDecimals = TOKEN_CONFIG[tokenInSymbol]?.decimals ?? 6; + + console.log(` [~] Buying ${ethers.utils.formatUnits(amountOut, outDecimals)} ${tokenOutSymbol} from Uniswap V3 (fee tier: ${feeTier / 10000}%)`); + console.log(` [~] Max spending: ${ethers.utils.formatUnits(maxAmountIn, inDecimals)} ${tokenInSymbol}`); + + await checkPoolLiquidity(provider, tokenIn, tokenOut, feeTier); + + if (dryRun) { + console.log(` [DRY-RUN] Would buy ${tokenOutSymbol} via Uniswap V3`); + return; + } + + await approveToken(signer, provider, tokenIn, tokenInSymbol, uniswapConfig.swapRouter02, maxAmountIn); + + if (signer.mode === 'privateKey') { + const router = new ethers.Contract(uniswapConfig.swapRouter02, UniswapSwapRouter02ABI, signer.wallet); + let tx; + try { + tx = await router.exactOutputSingle({ + tokenIn, + tokenOut, + fee: feeTier, + recipient: signer.address, + amountOut, + amountInMaximum: maxAmountIn, + sqrtPriceLimitX96: 0 + }, { nonce: signer.nextNonce() }); + } catch (err) { + await diagnoseGasError(err, signer, provider); + throw err; + } + await tx.wait(); + console.log(` [+] Bought ${tokenOutSymbol} via Uniswap (tx: ${tx.hash})`); + } else { + // cast send with tuple arg: "(tokenIn,tokenOut,fee,recipient,amountOut,amountInMaximum,sqrtLimit)" + const tupleArg = `"(${tokenIn},${tokenOut},${feeTier},${signer.address},${amountOut.toString()},${maxAmountIn.toString()},0)"`; + runCast( + signer, + uniswapConfig.swapRouter02, + 'exactOutputSingle((address,address,uint24,address,uint256,uint256,uint160))', + tupleArg + ); + console.log(` [+] Bought ${tokenOutSymbol} via Uniswap`); + } +} + +// ============================================================================ +// REBALANCING LOGIC +// ============================================================================ + +/** Query the current pool price deviation for a single token vs CoinGecko. */ +async function getTokenDeviation(partyInfo, poolAddr, token, quoteToken, usdPrices) { + const currentQ128BN = await partyInfo.price(poolAddr, token.idx, quoteToken.idx); + const currentQ128 = BigInt(currentQ128BN.toString()); + // partyInfo.price returns r0 * 10^(quoteDec-baseDec) in Q128 format. + const currentQ64n = currentQ128 / Q64; + const poolPriceRaw = Number(currentQ64n) / Number(Q64); + const poolPriceHuman = poolPriceRaw * Math.pow(10, token.decimals - quoteToken.decimals); + const poolUSD = poolPriceHuman * usdPrices[quoteToken.symbol]; + const targetPriceHuman = usdPrices[token.symbol] / usdPrices[quoteToken.symbol]; + const deviation = targetPriceHuman === 0 ? 0 + : (poolPriceHuman - targetPriceHuman) / targetPriceHuman * 100; + return { token, deviation, deviationAbs: Math.abs(deviation), poolUSD }; +} + +/** + * Execute a rebalancing swap for a single token via pool.swap(). + * Returns true if a swap was executed, false if price is already within threshold. + */ +async function rebalanceTokenIfNeeded( + { signer, provider, partyInfo, poolAddr, quoteToken, usdPrices, dryRun, denominators }, + token +) { + const currentQ128BN = await partyInfo.price(poolAddr, token.idx, quoteToken.idx); + const currentQ128 = BigInt(currentQ128BN.toString()); + const currentQ64n = currentQ128 / Q64; + const poolPriceRaw = Number(currentQ64n) / Number(Q64); + const poolPriceHuman = poolPriceRaw * Math.pow(10, token.decimals - quoteToken.decimals); + + const targetPriceHuman = usdPrices[token.symbol] / usdPrices[quoteToken.symbol]; + const deviation = targetPriceHuman === 0 ? 0 + : (poolPriceHuman - targetPriceHuman) / targetPriceHuman * 100; + + if (Math.abs(deviation) < PRICE_DEVIATION_THRESHOLD * 100) { + return false; + } + + // LMSR behaves like a regular AMM: injecting a token makes it cheaper. + // Overpriced base: inject TOKEN, extract QUOTE → lowers TOKEN price. + // Underpriced base: inject QUOTE, extract TOKEN → raises TOKEN price. + const overpriced = poolPriceHuman > targetPriceHuman; + const inputToken = overpriced ? token : quoteToken; + const outputToken = overpriced ? quoteToken : token; + const inputIdx = overpriced ? token.idx : quoteToken.idx; + const outputIdx = overpriced ? quoteToken.idx : token.idx; + const direction = overpriced ? 'lower-price' : 'raise-price'; + + console.log(` → ${direction}: inject ${inputToken.symbol}, extract ${outputToken.symbol}`); + + // Compute raw internal Q64.64 r0 for the swap direction using actual pool denominators. + // partyInfo.price(i,j) = r0_internal(i,j) × D[j] / D[i] in Q128.128 + // → r0_internal Q64.64 = price_Q128 × D[i] / D[j] / Q64 + const r0PriceQ128 = BigInt((await partyInfo.price(poolAddr, inputIdx, outputIdx)).toString()); + const r0InitialQ64 = r0PriceQ128 * denominators[inputIdx] / denominators[outputIdx] / Q64; + + // Scale r0_initial to get r0_target using the human-price ratio in the swap direction. + // overpriced (TOKEN→QUOTE): target price is lower → ratio = target/current < 1 + // underpriced (QUOTE→TOKEN): we want lower r0(QUOTE,TOKEN) → ratio = current/target < 1 + const numPrice = overpriced ? targetPriceHuman : poolPriceHuman; + const denPrice = overpriced ? poolPriceHuman : targetPriceHuman; + const r0TargetQ64 = r0InitialQ64 * BigInt(Math.round(numPrice * 1e9)) / BigInt(Math.round(denPrice * 1e9)); + + const limitPriceQ64 = computeLimitPrice(r0InitialQ64, r0TargetQ64); + const r0iFloat = Number(r0InitialQ64) / Number(Q64); + const r0tFloat = Number(r0TargetQ64) / Number(Q64); + console.log(` r0Initial=${r0iFloat.toExponential(4)} r0Target=${r0tFloat.toExponential(4)} limitPrice=${limitPriceQ64.toString()}`); + + const REDUCE_SIZE_RETRIES = 5; + let amountIn, amountOut, inFee, effectiveLimitPrice; + let r0TargetRetry = r0TargetQ64; + for (let attempt = 0; ; attempt++) { + const retryLimitPrice = attempt === 0 ? limitPriceQ64 : computeLimitPrice(r0InitialQ64, r0TargetRetry); + try { + const result = await partyInfo.swapToLimitAmounts(poolAddr, inputIdx, outputIdx, retryLimitPrice.toString()); + amountIn = result.amountIn; + amountOut = result.amountOut; + inFee = result.inFee; + effectiveLimitPrice = retryLimitPrice; + break; + } catch (err) { + if ( + attempt < REDUCE_SIZE_RETRIES && + err.message.includes('arithmetic underflow or overflow') + ) { + r0TargetRetry = r0InitialQ64 - (r0InitialQ64 - r0TargetRetry) / 10n; + console.log(` [!] swapToLimitAmounts overflow — retrying with reduced target (attempt ${attempt + 2}/${REDUCE_SIZE_RETRIES + 1})`); + continue; + } + console.log(` [!] swapToLimitAmounts reverted: ${err.message} — skipping`); + return false; + } + } + + if (amountIn.isZero()) { + console.log(` [+] amountIn=0, nothing to do`); + return false; + } + + console.log(` amountIn: ${ethers.utils.formatUnits(amountIn, inputToken.decimals)} ${inputToken.symbol}`); + console.log(` amountOut: ${ethers.utils.formatUnits(amountOut, outputToken.decimals)} ${outputToken.symbol}`); + console.log(` fee: ${ethers.utils.formatUnits(inFee, inputToken.decimals)} ${inputToken.symbol}`); + + // Ensure the signer holds enough of the input token; buy shortfall from Uniswap if needed + const inputERC20 = new ethers.Contract(inputToken.address, ERC20ABI, provider); + const inputBalance = await inputERC20.balanceOf(signer.address); + + if (inputBalance.lt(amountIn)) { + const deficit = amountIn.sub(inputBalance); + console.log(` [!] Insufficient ${inputToken.symbol}: ` + + `have ${ethers.utils.formatUnits(inputBalance, inputToken.decimals)}, ` + + `need ${ethers.utils.formatUnits(amountIn, inputToken.decimals)}, ` + + `deficit ${ethers.utils.formatUnits(deficit, inputToken.decimals)}`); + + if (inputToken.symbol === quoteToken.symbol) { + console.log(` [!] Input is the quote token — no way to acquire more, skipping`); + return false; + } + + const deficitUSD = Number(ethers.utils.formatUnits(deficit, inputToken.decimals)) * usdPrices[inputToken.symbol]; + const maxQuoteIn = ethers.utils.parseUnits( + (deficitUSD * 1.02 / usdPrices[quoteToken.symbol]).toFixed(quoteToken.decimals), + quoteToken.decimals + ); + + await buyFromUniswap( + signer, provider, + inputToken.address, inputToken.symbol, deficit, + quoteToken.address, quoteToken.symbol, maxQuoteIn, + dryRun + ); + } + + console.log(` [~] Approving ${inputToken.symbol} for pool...`); + if (!dryRun) { + await approveToken(signer, provider, inputToken.address, inputToken.symbol, poolAddr, amountIn.mul(101).div(100)); + } else { + console.log(` [DRY-RUN] Would approve ${inputToken.symbol}`); + } + + // Execute via pool.swap() with the computed amountIn and the effective limit price. + const deadline = Math.floor(Date.now() / 1000) + 300; + + if (!dryRun) { + if (signer.mode === 'privateKey') { + const poolContract = new ethers.Contract(poolAddr, [ + { type: 'function', name: 'swap', stateMutability: 'payable', + inputs: [ + { name: 'payer', type: 'address' }, { name: 'fundingSelector', type: 'bytes4' }, + { name: 'receiver', type: 'address' }, { name: 'inputTokenIndex', type: 'uint256' }, + { name: 'outputTokenIndex', type: 'uint256' }, { name: 'maxAmountIn', type: 'uint256' }, + { name: 'limitPrice', type: 'int128' }, { name: 'deadline', type: 'uint256' }, + { name: 'unwrap', type: 'bool' }, { name: 'cbData', type: 'bytes' } + ], + outputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'amountOut', type: 'uint256' }, { name: 'inFee', type: 'uint256' }] + } + ], signer.wallet); + let tx; + let swapAmountIn = amountIn; + for (let attempt = 0; ; attempt++) { + const nonce = signer.nextNonce(); + try { + tx = await poolContract.swap( + signer.address, '0x00000000', signer.address, + inputIdx, outputIdx, swapAmountIn, effectiveLimitPrice.toString(), deadline, false, '0x', + { nonce } + ); + break; + } catch (err) { + if ( + attempt < REDUCE_SIZE_RETRIES && + err.code === 'UNPREDICTABLE_GAS_LIMIT' && + err.message.includes('arithmetic underflow or overflow') + ) { + signer._nonce--; // tx never submitted; reuse nonce + swapAmountIn = swapAmountIn.div(10); + console.log(` [!] Arithmetic overflow — retrying with ${ethers.utils.formatUnits(swapAmountIn, inputToken.decimals)} ${inputToken.symbol} (attempt ${attempt + 2}/${REDUCE_SIZE_RETRIES})`); + continue; + } + await diagnoseGasError(err, signer, provider); + throw err; + } + } + await tx.wait(); + console.log(` [+] Swap executed (tx: ${tx.hash})`); + } else { + runCast( + signer, poolAddr, + 'swap(address,bytes4,address,uint256,uint256,uint256,int128,uint256,bool,bytes)', + `${signer.address} "0x00000000" ${signer.address} ${inputIdx} ${outputIdx} ${amountIn.toString()} ${effectiveLimitPrice.toString()} ${deadline} false "0x"` + ); + console.log(` [+] Swap executed`); + } + + const newQ128BN = await partyInfo.price(poolAddr, token.idx, quoteToken.idx); + const newQ64n = BigInt(newQ128BN.toString()) / Q64; + const newPoolUSD = Number(newQ64n) / Number(Q64) + * Math.pow(10, token.decimals - quoteToken.decimals) + * usdPrices[quoteToken.symbol]; + const residual = (newPoolUSD - usdPrices[token.symbol]) / usdPrices[token.symbol] * 100; + console.log(` [+] Updated pool price: $${newPoolUSD.toPrecision(6)} (market: $${usdPrices[token.symbol].toPrecision(6)} residual: ${residual.toFixed(3)}%)`); + } else { + console.log(` [DRY-RUN] Would swap ${inputToken.symbol} → ${outputToken.symbol} via pool.swap()`); + } + + return true; +} + +// ============================================================================ +// CLI ARGUMENT PARSING +// ============================================================================ + +function parseArgs() { + const args = process.argv.slice(2); + + if (args.includes('--help') || args.includes('-h')) { + console.log(` +Usage: node adjust_pool_prices_and_stake.js --pool
(--private-key | --trezor
) [OPTIONS] + +Signer (exactly one required): + --private-key Sign all transactions with this private key + --trezor
Sign all transactions via Trezor hardware wallet at given address + +Options: + --pool
Target LiqP pool address (required) + --quote Quote token symbol (default: auto-detect USDT/USDC) + --iterations Max rebalancing iterations, best-first (default: 10) + --stake USD amount to basket-stake via mint() after rebalancing + --skip-rebalance Skip price adjustment; only run staking (requires --stake) + --dry-run Preview only, no transactions executed + --help, -h Show this help message + +Environment: + NETWORK 'mainnet' (default) or 'mockchain' + MAINNET_RPC_URL RPC endpoint (required for mainnet) + +Examples: + node adjust_pool_prices_and_stake.js --pool 0x1234... --private-key 0xabc... --dry-run + node adjust_pool_prices_and_stake.js --pool 0x1234... --trezor 0xdef... --stake 500 + node adjust_pool_prices_and_stake.js --pool 0x1234... --trezor 0xdef... --stake 500 --skip-rebalance +`); + process.exit(0); + } + + const opts = { + pool: null, quote: null, stake: null, iterations: 10, + privateKey: null, trezorAddress: null, + skipRebalance: false, dryRun: false + }; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--pool' && i + 1 < args.length) { opts.pool = args[++i]; } + else if (args[i] === '--quote' && i + 1 < args.length) { opts.quote = args[++i].toUpperCase(); } + else if (args[i] === '--iterations' && i + 1 < args.length) { opts.iterations = parseInt(args[++i], 10); } + else if (args[i] === '--stake' && i + 1 < args.length) { opts.stake = parseFloat(args[++i]); } + else if (args[i] === '--private-key' && i + 1 < args.length) { opts.privateKey = args[++i]; } + else if (args[i] === '--trezor' && i + 1 < args.length) { opts.trezorAddress = args[++i]; } + else if (args[i] === '--skip-rebalance') { opts.skipRebalance = true; } + else if (args[i] === '--dry-run') { opts.dryRun = true; } + } + + if (!opts.pool) { + console.error('[!] --pool
is required.'); + process.exit(1); + } + if (!opts.privateKey && !opts.trezorAddress) { + console.error('[!] Signer required: provide --private-key or --trezor
.'); + process.exit(1); + } + if (opts.privateKey && opts.trezorAddress) { + console.error('[!] Provide --private-key OR --trezor, not both.'); + process.exit(1); + } + if (opts.skipRebalance && !opts.stake) { + console.error('[!] --skip-rebalance requires --stake .'); + process.exit(1); + } + + return opts; +} + +// ============================================================================ +// MAIN +// ============================================================================ + +async function main() { + const opts = parseArgs(); + const { pool: poolAddr, dryRun } = opts; + + const signerLabel = opts.trezorAddress ? `trezor:${opts.trezorAddress}` : 'private-key'; + console.log(`${'='.repeat(70)}`); + console.log(`Adjust Pool Prices`); + console.log(`Network: ${NETWORK} Pool: ${poolAddr} Signer: ${signerLabel}${opts.skipRebalance ? ' [STAKE-ONLY]' : ''}${dryRun ? ' [DRY-RUN]' : ''}`); + console.log(`${'='.repeat(70)}\n`); + + if (!currentConfig.rpcUrl) { + console.error(`[!] Missing RPC URL for network '${NETWORK}' (set MAINNET_RPC_URL)`); + process.exit(1); + } + + // ── Phase 0: Setup ───────────────────────────────────────────────────────── + + const provider = new ethers.providers.JsonRpcProvider(currentConfig.rpcUrl); + const signer = buildSigner(opts, provider); + console.log(`[+] Signer: ${signer.address} (${signer.mode})`); + + if (!dryRun) { + signer._nonce = await provider.getTransactionCount(signer.address, "pending"); + console.log(`[+] Nonce: ${signer._nonce} (pending)`); + } else { + signer._nonce = 0; + } + signer.nextNonce = () => signer._nonce++; + + const chainInfoData = JSON.parse( + await readFile(new URL('../src/contracts/liqp-deployments.json', import.meta.url), 'utf-8') + ); + const chainContracts = chainInfoData[currentConfig.chainId]?.v1; + if (!chainContracts) { + console.error(`[!] No contract addresses for chainId ${currentConfig.chainId}`); + process.exit(1); + } + const PARTY_INFO_ADDRESS = chainContracts.PartyInfo; + console.log(`[+] PartyInfo: ${PARTY_INFO_ADDRESS}`); + + const pool = new ethers.Contract(poolAddr, IPartyPoolABI, provider); + const partyInfo = new ethers.Contract(PARTY_INFO_ADDRESS, IPartyInfoABI, provider); + + const denomsBN = await pool.denominators(); + const denominators = denomsBN.map(d => BigInt(d.toString())); + console.log(`[+] Denominators: [${denominators.join(', ')}]`); + + const tokenAddresses = await pool.allTokens(); + console.log(`[+] Pool tokens: ${tokenAddresses.length}`); + + const tokenConfigByAddr = {}; + for (const [sym, info] of Object.entries(TOKEN_CONFIG)) { + tokenConfigByAddr[info.address.toLowerCase()] = { symbol: sym, ...info }; + } + + const poolTokens = tokenAddresses.map((addr, idx) => { + const meta = tokenConfigByAddr[addr.toLowerCase()]; + if (!meta) throw new Error(`Unknown token at pool index ${idx}: ${addr}`); + return { idx, address: addr, symbol: meta.symbol, decimals: meta.decimals, coingeckoId: meta.coingeckoId }; + }); + + let quoteToken; + if (opts.quote) { + quoteToken = poolTokens.find(t => t.symbol === opts.quote); + if (!quoteToken) throw new Error(`Quote token ${opts.quote} not found in pool`); + } else { + quoteToken = poolTokens.find(t => t.symbol === 'USDT') ?? poolTokens.find(t => t.symbol === 'USDC'); + if (!quoteToken) throw new Error('No USDT or USDC in pool; specify --quote '); + } + console.log(`[+] Quote: ${quoteToken.symbol} (index ${quoteToken.idx})\n`); + + const nonQuoteTokens = poolTokens.filter(t => t.idx !== quoteToken.idx); + + // ── Phase 1: Fetch Prices ─────────────────────────────────────────────────── + + const symbols = poolTokens.map(t => t.symbol); + const usdPrices = await fetchCoinGeckoPrices(symbols); + + console.log(`\n[+] Market prices:`); + for (const sym of symbols) { + console.log(` ${sym.padEnd(6)}: $${usdPrices[sym].toLocaleString()}`); + } + + const ctx = { signer, provider, partyInfo, poolAddr, poolTokens, quoteToken, usdPrices, dryRun, denominators }; + + // ── Phase 2: Rebalancing — best-first iterative (skipped with --skip-rebalance) ─ + + let swapCount = 0; + + if (opts.skipRebalance) { + console.log(`\n[~] Skipping price adjustment (--skip-rebalance).`); + } else { + console.log(`\n${'─'.repeat(70)}`); + console.log(`Rebalancing — best-first, up to ${opts.iterations} iterations (threshold: ${PRICE_DEVIATION_THRESHOLD * 100}%)`); + console.log(`${'─'.repeat(70)}`); + + let converged = false; + for (let iter = 1; iter <= opts.iterations; iter++) { + // Query all pool prices in parallel, find the most deviant token + const deviations = await Promise.all( + nonQuoteTokens.map(t => getTokenDeviation(partyInfo, poolAddr, t, quoteToken, usdPrices)) + ); + + console.log(`\n[iter ${iter}/${opts.iterations}]`); + for (const d of deviations) { + const marker = d.deviationAbs >= PRICE_DEVIATION_THRESHOLD * 100 ? '!' : '='; + console.log(` [${marker}] ${d.token.symbol.padEnd(6)} pool=$${d.poolUSD.toPrecision(6)} market=$${usdPrices[d.token.symbol].toPrecision(6)} deviation=${d.deviation.toFixed(3)}%`); + } + + const worst = deviations.reduce((a, b) => a.deviationAbs > b.deviationAbs ? a : b); + + if (worst.deviationAbs < PRICE_DEVIATION_THRESHOLD * 100) { + console.log(`\n[+] All prices within threshold — converged after ${swapCount} swap(s).`); + converged = true; + break; + } + + console.log(`\n Swapping ${worst.token.symbol} (${worst.deviation.toFixed(3)}% off)`); + const swapped = await rebalanceTokenIfNeeded(ctx, worst.token); + if (swapped) swapCount++; + } + + if (!converged) { + console.log(`\n[~] Reached iteration limit (${opts.iterations}) with ${swapCount} swap(s) executed.`); + } + } + + // ── Phase 4: Optional Basket Stake via mint() ────────────────────────────── + + if (opts.stake) { + console.log(`\n${'='.repeat(70)}`); + console.log(`Phase 4: Basket Stake ($${opts.stake} USD)`); + console.log(`${'='.repeat(70)}`); + + const totalSupplyBN = await pool.totalSupply(); + const balancesBN = await pool.balances(); + + let tvlUSD = 0; + for (let i = 0; i < poolTokens.length; i++) { + tvlUSD += Number(ethers.utils.formatUnits(balancesBN[i], poolTokens[i].decimals)) * usdPrices[poolTokens[i].symbol]; + } + + const totalSupplyFloat = Number(ethers.utils.formatUnits(totalSupplyBN, 18)); + const lpPriceUSD = tvlUSD / totalSupplyFloat; + console.log(`\n[~] Pool TVL: $${tvlUSD.toFixed(2)}, LP price: $${lpPriceUSD.toFixed(6)}`); + + const lpAmountFloat = opts.stake / lpPriceUSD; + const lpAmount = ethers.utils.parseUnits(lpAmountFloat.toFixed(18), 18); + console.log(`[~] Target LP amount: ${lpAmountFloat.toFixed(6)} LP tokens (~$${opts.stake})`); + + const depositAmounts = await partyInfo.mintAmounts(poolAddr, lpAmount); + console.log(`\n[~] Required deposits for mint:`); + + const deficits = []; + let totalQuoteNeeded = ethers.BigNumber.from(0); + + for (let i = 0; i < poolTokens.length; i++) { + const t = poolTokens[i]; + const required = depositAmounts[i]; + const tokenERC20 = new ethers.Contract(t.address, ERC20ABI, provider); + const balance = await tokenERC20.balanceOf(signer.address); + const sufficient = balance.gte(required); + + console.log( + ` ${t.symbol.padEnd(6)}: ` + + `${ethers.utils.formatUnits(required, t.decimals).padStart(22)} required ` + + `${ethers.utils.formatUnits(balance, t.decimals).padStart(22)} in wallet ` + + `${sufficient ? '✓' : '✗'}` + ); + + if (!sufficient) { + const deficit = required.sub(balance); + const deficitUSD = Number(ethers.utils.formatUnits(deficit, t.decimals)) * usdPrices[t.symbol]; + const quoteNeeded = ethers.utils.parseUnits( + (deficitUSD * 1.02 / usdPrices[quoteToken.symbol]).toFixed(quoteToken.decimals), + quoteToken.decimals + ); + totalQuoteNeeded = totalQuoteNeeded.add(quoteNeeded); + deficits.push({ token: t, deficit, quoteNeeded }); + } + } + + // Pre-flight: fail before any transaction if quote balance is insufficient + if (deficits.length > 0) { + const quoteERC20 = new ethers.Contract(quoteToken.address, ERC20ABI, provider); + const quoteBalance = await quoteERC20.balanceOf(signer.address); + + if (quoteBalance.lt(totalQuoteNeeded)) { + console.error(`\n[!] Pre-flight check FAILED — insufficient ${quoteToken.symbol} to cover token deficits:`); + for (const d of deficits) { + console.error(` ${d.token.symbol}: deficit ${ethers.utils.formatUnits(d.deficit, d.token.decimals)}` + + ` → costs ~${ethers.utils.formatUnits(d.quoteNeeded, quoteToken.decimals)} ${quoteToken.symbol}`); + } + console.error(` Total needed: ${ethers.utils.formatUnits(totalQuoteNeeded, quoteToken.decimals)} ${quoteToken.symbol}`); + console.error(` Wallet has: ${ethers.utils.formatUnits(quoteBalance, quoteToken.decimals)} ${quoteToken.symbol}`); + process.exit(1); + } + console.log(`\n[+] Pre-flight check passed`); + } + + if (dryRun) { + console.log(`\n[DRY-RUN] Would buy ${deficits.length} deficit token(s) and mint ${lpAmountFloat.toFixed(6)} LP`); + } else { + for (const d of deficits) { + console.log(`\n[~] Buying ${d.token.symbol} deficit from Uniswap...`); + await buyFromUniswap( + signer, provider, + d.token.address, d.token.symbol, d.deficit, + quoteToken.address, quoteToken.symbol, d.quoteNeeded, + false + ); + } + + console.log(`\n[~] Approving tokens for pool mint...`); + for (let i = 0; i < poolTokens.length; i++) { + const t = poolTokens[i]; + const required = depositAmounts[i]; + if (required.gt(0)) { + await approveToken(signer, provider, t.address, t.symbol, poolAddr, required.mul(101).div(100)); + } + } + + const deadline = Math.floor(Date.now() / 1000) + 300; + + if (signer.mode === 'privateKey') { + const poolContract = new ethers.Contract(poolAddr, [ + { type: 'function', name: 'mint', stateMutability: 'payable', + inputs: [ + { name: 'payer', type: 'address' }, { name: 'receiver', type: 'address' }, + { name: 'lpTokenAmount', type: 'uint256' }, { name: 'deadline', type: 'uint256' } + ], + outputs: [{ name: 'lpMinted', type: 'uint256' }] + } + ], signer.wallet); + let tx; + try { + tx = await poolContract.mint(signer.address, signer.address, lpAmount, deadline, { nonce: signer.nextNonce() }); + } catch (err) { + await diagnoseGasError(err, signer, provider); + throw err; + } + await tx.wait(); + console.log(`[+] Mint executed (tx: ${tx.hash})`); + } else { + runCast( + signer, poolAddr, + 'mint(address,address,uint256,uint256)', + `${signer.address} ${signer.address} ${lpAmount.toString()} ${deadline}` + ); + console.log(`[+] Mint executed`); + } + + const newLpBalance = await pool.balanceOf(signer.address); + console.log(`[+] Wallet LP balance: ${ethers.utils.formatUnits(newLpBalance, 18)}`); + } + } + + // ── Summary ──────────────────────────────────────────────────────────────── + + console.log(`\n${'='.repeat(70)}`); + console.log(`Summary`); + console.log(`${'='.repeat(70)}`); + if (opts.skipRebalance) { + console.log(`Rebalance: skipped (--skip-rebalance)`); + } else { + console.log(`Rebalance: ${swapCount} swap(s), up to ${opts.iterations} iterations${dryRun ? ' (dry-run)' : ''}`); + } + if (opts.stake) { + console.log(`Stake: $${opts.stake} USD${dryRun ? ' (dry-run)' : ''}`); + } + console.log(`Done.`); +} + +main().catch(err => { + console.error('[!] Fatal error:', err.message || err); + process.exit(1); +}); diff --git a/scripts/create_pool_from_prices.js b/scripts/create_pool_from_prices.js index e968f08..e0e0b9f 100644 --- a/scripts/create_pool_from_prices.js +++ b/scripts/create_pool_from_prices.js @@ -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 amount to distribute (default: ${INPUT_USD_AMOUNT}) - --name Pool name (default: "${DEFAULT_POOL_PARAMS.name}") - --symbol Pool symbol (default: "${DEFAULT_POOL_PARAMS.symbol}") + --config Pool config JSON file (default: pool-og.json) + --amount USD amount to distribute (overrides config, default: ${INPUT_USD_AMOUNT}) + --name Pool name (overrides config) + --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); -}); \ No newline at end of file +}); diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 0000000..96ad3a3 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,894 @@ +{ + "name": "liquidity-party-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "liquidity-party-scripts", + "version": "1.0.0", + "dependencies": { + "dotenv": "^17.2.3", + "ethers": "^5.7.2" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", + "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz", + "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.8.0.tgz", + "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz", + "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz", + "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", + "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz", + "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz", + "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz", + "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "license": "MIT" + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "license": "MIT" + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/scripts/package.json b/scripts/package.json index c46b248..928c7bc 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -5,7 +5,8 @@ "type": "module", "private": true, "scripts": { - "create-pool": "node create_pool_from_prices.js" + "create-pool": "node create_pool_from_prices.js", + "adjust-pool": "node adjust_pool_prices_and_stake.js" }, "dependencies": { "dotenv": "^17.2.3", diff --git a/scripts/pool-og.json b/scripts/pool-og.json new file mode 100644 index 0000000..d465205 --- /dev/null +++ b/scripts/pool-og.json @@ -0,0 +1,70 @@ +{ + "name": "Original Genesis of Liquidity Party", + "symbol": "OG.LP", + "kappa": 0.01, + "flashFeePpm": 5, + "inputUsdAmount": 75, + "stable": false, + "tokens": { + "USDT": { + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "coingeckoId": "tether", + "decimals": 6, + "feePpm": 40 + }, + "USDC": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "coingeckoId": "usd-coin", + "decimals": 6, + "feePpm": 40 + }, + "WBTC": { + "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "coingeckoId": "wrapped-bitcoin", + "decimals": 8, + "feePpm": 200 + }, + "WETH": { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "coingeckoId": "weth", + "decimals": 18, + "feePpm": 250 + }, + "UNI": { + "address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "coingeckoId": "uniswap", + "decimals": 18, + "feePpm": 950 + }, + "WSOL": { + "address": "0xD31a59c85aE9D8edEFeC411D448f90841571b89c", + "coingeckoId": "solana", + "decimals": 9, + "feePpm": 950 + }, + "TRX": { + "address": "0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5", + "coingeckoId": "tron", + "decimals": 6, + "feePpm": 950 + }, + "AAVE": { + "address": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", + "coingeckoId": "aave", + "decimals": 18, + "feePpm": 1250 + }, + "PEPE": { + "address": "0x6982508145454Ce325dDbE47a25d4ec3d2311933", + "coingeckoId": "pepe", + "decimals": 18, + "feePpm": 1850 + }, + "SHIB": { + "address": "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", + "coingeckoId": "shiba-inu", + "decimals": 18, + "feePpm": 1850 + } + } +} diff --git a/scripts/pool-test.json b/scripts/pool-test.json new file mode 100644 index 0000000..d420739 --- /dev/null +++ b/scripts/pool-test.json @@ -0,0 +1,28 @@ +{ + "name": "Test Pool", + "symbol": "TEST.LP", + "kappa": 0.1, + "flashFeePpm": 5, + "inputUsdAmount": 3, + "stable": false, + "tokens": { + "USDT": { + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "coingeckoId": "tether", + "decimals": 6, + "feePpm": 40 + }, + "WETH": { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "coingeckoId": "weth", + "decimals": 18, + "feePpm": 250 + }, + "AAVE": { + "address": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", + "coingeckoId": "aave", + "decimals": 18, + "feePpm": 1250 + } + } +} diff --git a/src/components/stake-form.tsx b/src/components/stake-form.tsx index 258c9b8..b5a50ff 100644 --- a/src/components/stake-form.tsx +++ b/src/components/stake-form.tsx @@ -426,7 +426,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { {pool.symbol} {pool.name} - {(pool.price || pool.tvl) && ( + {(pool.price || pool.tvl || pool.apy) && (
{pool.price && ( {pool.price} @@ -434,6 +434,9 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { {pool.tvl && ( TVL: {pool.tvl} )} + {pool.apy && ( + APY: {pool.apy} + )}
)} @@ -502,7 +505,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { {pool.name} - {!pool.isKilled && (pool.price || pool.tvl) && ( + {!pool.isKilled && (pool.price || pool.tvl || pool.apy) && (
{pool.price && ( {pool.price} @@ -510,6 +513,9 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { {pool.tvl && ( TVL: {pool.tvl} )} + {pool.apy && ( + APY: {pool.apy} + )}
)} diff --git a/src/contracts/IPartyInfoABI.ts b/src/contracts/IPartyInfoABI.ts index 4b30395..423da97 100644 --- a/src/contracts/IPartyInfoABI.ts +++ b/src/contracts/IPartyInfoABI.ts @@ -170,12 +170,12 @@ const IPartyInfoABI = [ "internalType": "contract IPartyPool" }, { - "name": "baseTokenIndex", + "name": "inputTokenIndex", "type": "uint256", "internalType": "uint256" }, { - "name": "quoteTokenIndex", + "name": "outputTokenIndex", "type": "uint256", "internalType": "uint256" } @@ -189,6 +189,94 @@ const IPartyInfoABI = [ ], "stateMutability": "view" }, + { + "type": "function", + "name": "swapAmounts", + "inputs": [ + { + "name": "pool", + "type": "address", + "internalType": "contract IPartyPool" + }, + { + "name": "inputTokenIndex", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "outputTokenIndex", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxAmountIn", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "amountOut", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "inFee", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "swapAmountsForExactPrice", + "inputs": [ + { + "name": "pool", + "type": "address", + "internalType": "contract IPartyPool" + }, + { + "name": "inputTokenIndex", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "outputTokenIndex", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minPrice", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "amountOut", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "inFee", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "swapMintAmounts", @@ -228,50 +316,6 @@ const IPartyInfoABI = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "swapToLimitAmounts", - "inputs": [ - { - "name": "pool", - "type": "address", - "internalType": "contract IPartyPool" - }, - { - "name": "inputTokenIndex", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "outputTokenIndex", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limitPrice", - "type": "int128", - "internalType": "int128" - } - ], - "outputs": [ - { - "name": "amountIn", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "amountOut", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "inFee", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "working", diff --git a/src/contracts/IPartyPlannerABI.ts b/src/contracts/IPartyPlannerABI.ts index c151b5d..0b7401b 100644 --- a/src/contracts/IPartyPlannerABI.ts +++ b/src/contracts/IPartyPlannerABI.ts @@ -1,6 +1,13 @@ /* GENERATED FILE: DO NOT EDIT! */ const IPartyPlannerABI = [ + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "getAllPools", @@ -97,19 +104,6 @@ const IPartyPlannerABI = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "mintImpl", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract PartyPoolMintImpl" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "newPool", @@ -365,6 +359,19 @@ const IPartyPlannerABI = [ ], "stateMutability": "view" }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "poolCount", @@ -399,28 +406,32 @@ const IPartyPlannerABI = [ }, { "type": "function", - "name": "renounceOwnership", - "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "swapImpl", + "name": "tokenCount", "inputs": [], "outputs": [ { "name": "", - "type": "address", - "internalType": "contract PartyPoolSwapImpl" + "type": "uint256", + "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", - "name": "tokenCount", - "inputs": [], + "name": "tokenIndex", + "inputs": [ + { + "name": "pool", + "type": "address", + "internalType": "contract IPartyPool" + }, + { + "name": "token", + "type": "address", + "internalType": "contract IERC20" + } + ], "outputs": [ { "name": "", @@ -443,6 +454,25 @@ const IPartyPlannerABI = [ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, { "type": "event", "name": "OwnershipTransferred", diff --git a/src/contracts/IPartyPoolABI.ts b/src/contracts/IPartyPoolABI.ts index d7c9ac5..1c8f0b0 100644 --- a/src/contracts/IPartyPoolABI.ts +++ b/src/contracts/IPartyPoolABI.ts @@ -26,6 +26,13 @@ const IPartyPoolABI = [ ], "stateMutability": "view" }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "allProtocolFeesOwed", @@ -100,25 +107,6 @@ const IPartyPoolABI = [ ], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "balance", - "inputs": [ - { - "name": "index", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "balanceOf", @@ -214,6 +202,11 @@ const IPartyPoolABI = [ "type": "uint256", "internalType": "uint256" }, + { + "name": "minAmountOut", + "type": "uint256", + "internalType": "uint256" + }, { "name": "deadline", "type": "uint256", @@ -272,30 +265,6 @@ const IPartyPoolABI = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "fee", - "inputs": [ - { - "name": "i", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "j", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "fees", @@ -380,19 +349,6 @@ const IPartyPoolABI = [ ], "stateMutability": "payable" }, - { - "type": "function", - "name": "kappa", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "int128", - "internalType": "int128" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "kill", @@ -422,6 +378,11 @@ const IPartyPoolABI = [ "type": "address", "internalType": "address" }, + { + "name": "fundingSelector", + "type": "bytes4", + "internalType": "bytes4" + }, { "name": "receiver", "type": "address", @@ -436,6 +397,11 @@ const IPartyPoolABI = [ "name": "deadline", "type": "uint256", "internalType": "uint256" + }, + { + "name": "cbData", + "type": "bytes", + "internalType": "bytes" } ], "outputs": [ @@ -447,19 +413,6 @@ const IPartyPoolABI = [ ], "stateMutability": "payable" }, - { - "type": "function", - "name": "mintImpl", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "name", @@ -499,6 +452,19 @@ const IPartyPoolABI = [ ], "stateMutability": "view" }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "protocolFeeAddress", @@ -525,13 +491,6 @@ const IPartyPoolABI = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "renounceOwnership", - "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "swap", @@ -567,9 +526,9 @@ const IPartyPoolABI = [ "internalType": "uint256" }, { - "name": "limitPrice", - "type": "int128", - "internalType": "int128" + "name": "minAmountOut", + "type": "uint256", + "internalType": "uint256" }, { "name": "deadline", @@ -606,115 +565,9 @@ const IPartyPoolABI = [ ], "stateMutability": "payable" }, - { - "type": "function", - "name": "swapAmounts", - "inputs": [ - { - "name": "inputTokenIndex", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "outputTokenIndex", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "maxAmountIn", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limitPrice", - "type": "int128", - "internalType": "int128" - } - ], - "outputs": [ - { - "name": "amountIn", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "amountOut", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "inFee", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "swapImpl", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "swapMint", - "inputs": [ - { - "name": "payer", - "type": "address", - "internalType": "address" - }, - { - "name": "receiver", - "type": "address", - "internalType": "address" - }, - { - "name": "inputTokenIndex", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "maxAmountIn", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "deadline", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "amountInUsed", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "lpMinted", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "inFee", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "swapToLimit", "inputs": [ { "name": "payer", @@ -737,25 +590,20 @@ const IPartyPoolABI = [ "internalType": "uint256" }, { - "name": "outputTokenIndex", + "name": "maxAmountIn", "type": "uint256", "internalType": "uint256" }, { - "name": "limitPrice", - "type": "int128", - "internalType": "int128" + "name": "minLpOut", + "type": "uint256", + "internalType": "uint256" }, { "name": "deadline", "type": "uint256", "internalType": "uint256" }, - { - "name": "unwrap", - "type": "bool", - "internalType": "bool" - }, { "name": "cbData", "type": "bytes", @@ -769,7 +617,7 @@ const IPartyPoolABI = [ "internalType": "uint256" }, { - "name": "amountOut", + "name": "lpMinted", "type": "uint256", "internalType": "uint256" }, @@ -1090,6 +938,25 @@ const IPartyPoolABI = [ ], "anonymous": false }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, { "type": "event", "name": "OwnershipTransferred", diff --git a/src/hooks/usePartyPlanner.ts b/src/hooks/usePartyPlanner.ts index 8ff79ff..5338147 100644 --- a/src/hooks/usePartyPlanner.ts +++ b/src/hooks/usePartyPlanner.ts @@ -8,6 +8,7 @@ import IPartyPoolABI from '@/contracts/IPartyPoolABI'; import IPartyPoolViewerABI from '@/contracts/IPartyPoolViewerABI'; import IPartyInfoABI from '@/contracts/IPartyInfoABI'; import { ERC20ABI } from '@/contracts/ERC20ABI'; +import { fetchPoolMetrics } from '@/lib/poolMetricsCache'; // Helper function to format large numbers with K, M, B suffixes function formatTVL(value: number): string { @@ -444,6 +445,7 @@ export interface PoolDetails { tokens: readonly `0x${string}`[]; price?: string; // Formatted price string tvl?: string; // Formatted TVL string (e.g., "$1.2M") + apy?: string; // Formatted APY string (e.g., "3.25%") isKilled: boolean; // Whether the pool has been killed } @@ -495,6 +497,9 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) { setPools(result); + // Fetch Substreams pool metrics (cached for 1 hour) + const poolMetricsData = await fetchPoolMetrics(); + // Fetch details for each pool and check if it's working const details: PoolDetails[] = []; for (const poolAddress of result) { @@ -526,6 +531,7 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) { // Fetch pool price and TVL (only for working pools) let priceStr: string | undefined; let tvlStr: string | undefined; + let apyStr: string | undefined; if (isWorking) { // Fetch token decimals and balance first (needed for both price and TVL) @@ -567,10 +573,21 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) { priceStr = `$${finalPrice.toFixed(4)}`; } - // Calculate TVL (approximate by getting first token balance and multiplying by number of tokens) - const tokenBalance = Number(balance) / Math.pow(10, decimals); - const approximateTVL = tokenBalance * tokens.length; - tvlStr = formatTVL(approximateTVL); + // Use Substreams TVL + APY if available; fall back to on-chain estimate + const metricKey = poolAddress.toLowerCase().replace('0x', ''); + const metric = poolMetricsData[metricKey]; + if (metric) { + const tvlAdjusted = Number(BigInt(metric.quoteTvl)) / Math.pow(10, decimals); + tvlStr = formatTVL(tvlAdjusted); + if (metric.quoteApyBps > 0) { + apyStr = `${(metric.quoteApyBps / 100).toFixed(2)}%`; + } + } else { + // Fall back to on-chain estimate + const tokenBalance = Number(balance) / Math.pow(10, decimals); + const approximateTVL = tokenBalance * tokens.length; + tvlStr = formatTVL(approximateTVL); + } } } catch (err) { console.error(`Error fetching pool price/TVL for ${poolAddress}:`, err); @@ -587,6 +604,7 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) { tokens: tokens as readonly `0x${string}`[], price: priceStr, tvl: tvlStr, + apy: apyStr, isKilled: !isWorking, }); } catch (err) { diff --git a/src/lib/poolMetricsCache.ts b/src/lib/poolMetricsCache.ts new file mode 100644 index 0000000..5287009 --- /dev/null +++ b/src/lib/poolMetricsCache.ts @@ -0,0 +1,123 @@ +// Client-side cache for Substreams pool metrics (TVL + APY). +// Fetches from the configured GraphQL subgraph endpoint and caches results +// in memory and localStorage for up to 1 hour to minimize subgraph query costs. + +export interface PoolMetricEntry { + quoteTvl: string; // Raw bigint string — needs division by 10^token0Decimals + quoteApyBps: number; // Compound APY in basis points (divide by 100 for %) +} + +interface CacheData { + timestamp: number; + metrics: Record; // keyed by lowercase pool address (no 0x prefix) +} + +const CACHE_KEY = 'lp_pool_metrics_v1'; +const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour + +let memCache: CacheData | null = null; + +function loadFromStorage(): CacheData | null { + if (typeof localStorage === 'undefined') return null; + try { + const raw = localStorage.getItem(CACHE_KEY); + if (!raw) return null; + return JSON.parse(raw) as CacheData; + } catch { + return null; + } +} + +function saveToStorage(data: CacheData): void { + if (typeof localStorage === 'undefined') return; + try { + localStorage.setItem(CACHE_KEY, JSON.stringify(data)); + } catch { + // Ignore storage quota errors + } +} + +function isFresh(data: CacheData): boolean { + return Date.now() - data.timestamp < CACHE_TTL_MS; +} + +// In-flight promise to avoid duplicate simultaneous fetches +let inFlight: Promise> | null = null; + +export async function fetchPoolMetrics(): Promise> { + // Memory cache hit + if (memCache && isFresh(memCache)) { + return memCache.metrics; + } + + // localStorage cache hit (skip if empty — means a previous fetch returned nothing) + const stored = loadFromStorage(); + if (stored && isFresh(stored) && Object.keys(stored.metrics).length > 0) { + memCache = stored; + return stored.metrics; + } + + // Deduplicate concurrent fetches + if (inFlight) return inFlight; + + inFlight = (async () => { + const endpoint = process.env.NEXT_PUBLIC_SUBSTREAMS_ENDPOINT; + if (!endpoint) return {}; + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: `{ + poolMetrics(first: 100) { + id + quoteTvl + quoteApyBps + } + }`, + }), + }); + + if (!response.ok) { + console.warn('[poolMetrics] fetch failed:', response.status, response.statusText, await response.text().catch(() => '')); + return {}; + } + + const json = await response.json() as { + data?: { poolMetrics?: Array<{ id: string; quoteTvl: string; quoteApyBps: string | number }> }; + errors?: unknown; + }; + + if (json.errors) { + console.warn('[poolMetrics] GraphQL errors:', json.errors); + } + + const entries = json?.data?.poolMetrics ?? []; + console.log(`[poolMetrics] received ${entries.length} pool entries`); + + const metrics: Record = {}; + for (const entry of entries) { + metrics[entry.id.toLowerCase()] = { + quoteTvl: entry.quoteTvl, + quoteApyBps: Number(entry.quoteApyBps), + }; + } + + // Only cache non-empty results so a failed/empty response doesn't block retries + if (Object.keys(metrics).length > 0) { + const cache: CacheData = { timestamp: Date.now(), metrics }; + memCache = cache; + saveToStorage(cache); + } + return metrics; + } catch (err) { + console.warn('[poolMetrics] fetch error:', err); + return {}; + } finally { + inFlight = null; + } + })(); + + return inFlight; +}