Created
March 4, 2026 01:01
-
-
Save zhangchiqing/e868003ce3745b9f42d546d915cf8129 to your computer and use it in GitHub Desktop.
check_lp_balance
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * KittyPunch LP Balance Checker | |
| * | |
| * This script queries all major KittyPunch liquidity pools on Flow EVM | |
| * to find a user's LP token balances and calculate their underlying asset values. | |
| * | |
| * Usage: | |
| * node check_lp_balances.js [user_address] [block_height] | |
| * | |
| * If no address is provided, uses the default address. | |
| * If no block_height is provided, uses 'latest'. | |
| */ | |
| const https = require('https'); | |
| // Configuration | |
| const RPC_URL = 'https://mainnet.evm.nodes.onflow.org'; | |
| const FACTORY_ADDRESS = '0x29372c22459a4e373851798bFd6808e71EA34A71'; | |
| // Block parameter (set from command line, default to 'latest') | |
| let BLOCK_PARAM = 'latest'; | |
| // Token addresses on Flow EVM Mainnet | |
| const TOKENS = { | |
| WFLOW: '0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e', | |
| USDC: '0xF1815bd50389c46847f0Bda824eC8da914045D14', | |
| PYUSD0: '0x99aF3EeA856556646C98c8B9b2548Fe815240750', | |
| USDF: '0x2aaBea2058b5aC2D339b163C6Ab6f2b6d53aabED', | |
| USDC_e: '0x7f27352D5F83Db87a5A3E00f4B07Cc2138D8ee52', | |
| WETH: '0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590', | |
| ankrFLOW: '0x1b97100eA1D7126C4d60027e231EA4CB25314bdb' | |
| }; | |
| // Staked LP positions (gauge/farm contracts) | |
| // poolType: 'curve' for StableKitty (Curve-style), 'uniswap' for PunchSwap (Uniswap V2-style) | |
| const STAKED_POSITIONS = [ | |
| { | |
| name: 'USDF-stgUSDC (StableKitty)', | |
| gauge: '0xFCd501d350bf31c5a2E2Aa9C62DC1BEd5920d802', | |
| lpToken: '0x20ca5d1C8623ba6AC8f02E41cCAFFe7bb6C92B57', | |
| poolType: 'curve' | |
| }, | |
| { | |
| name: 'WFLOW-USDF (PunchSwap Farm)', | |
| gauge: '0xb334B50fc34005c87C7e6420E3E2D9a027E4D662', | |
| lpToken: '0x17e96496212d06Eb1Ff10C6f853669Cc9947A1e7', | |
| poolType: 'uniswap' | |
| }, | |
| { | |
| name: 'WFLOW-ankrFLOW (StableKitty)', | |
| gauge: '0x289ca59F51893d566255588F4C8407B07644f5D6', | |
| lpToken: '0x7296a9C350cad25fc69B47Ec839DCf601752C3C2', | |
| poolType: 'curve' | |
| } | |
| ]; | |
| // Pool pairs to check | |
| const POOL_PAIRS = [ | |
| {name: 'PYUSD0-USDC', token0Name: 'PYUSD0', token1Name: 'USDC', tokenA: TOKENS.PYUSD0, tokenB: TOKENS.USDC}, | |
| {name: 'USDF-USDC', token0Name: 'USDF', token1Name: 'USDC', tokenA: TOKENS.USDF, tokenB: TOKENS.USDC}, | |
| {name: 'WFLOW-USDF', token0Name: 'WFLOW', token1Name: 'USDF', tokenA: TOKENS.WFLOW, tokenB: TOKENS.USDF}, | |
| {name: 'WFLOW-ankrFLOW', token0Name: 'WFLOW', token1Name: 'ankrFLOW', tokenA: TOKENS.WFLOW, tokenB: TOKENS.ankrFLOW}, | |
| {name: 'WETH-WFLOW', token0Name: 'WETH', token1Name: 'WFLOW', tokenA: TOKENS.WETH, tokenB: TOKENS.WFLOW}, | |
| {name: 'WFLOW-USDC.e', token0Name: 'WFLOW', token1Name: 'USDC.e', tokenA: TOKENS.WFLOW, tokenB: TOKENS.USDC_e}, | |
| {name: 'WETH-USDF', token0Name: 'WETH', token1Name: 'USDF', tokenA: TOKENS.WETH, tokenB: TOKENS.USDF} | |
| ]; | |
| // Helper: Make JSON-RPC call | |
| function rpcCall(method, params) { | |
| return new Promise((resolve, reject) => { | |
| const data = JSON.stringify({ | |
| jsonrpc: '2.0', | |
| method: method, | |
| params: params, | |
| id: 1 | |
| }); | |
| const options = { | |
| hostname: 'mainnet.evm.nodes.onflow.org', | |
| port: 443, | |
| path: '/', | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Content-Length': data.length | |
| } | |
| }; | |
| const req = https.request(options, (res) => { | |
| let body = ''; | |
| res.on('data', (chunk) => body += chunk); | |
| res.on('end', () => { | |
| try { | |
| const response = JSON.parse(body); | |
| if (response.error) { | |
| reject(new Error(response.error.message)); | |
| } else { | |
| resolve(response.result); | |
| } | |
| } catch (error) { | |
| reject(error); | |
| } | |
| }); | |
| }); | |
| req.on('error', reject); | |
| req.write(data); | |
| req.end(); | |
| }); | |
| } | |
| // Helper: Encode function call | |
| function encodeFunctionCall(signature, params) { | |
| // Get function selector (first 4 bytes of keccak256 hash) | |
| const selector = signature.substring(0, 10); | |
| return selector + params.map(p => p.replace('0x', '').padStart(64, '0')).join(''); | |
| } | |
| // Helper: Decode uint256 | |
| function decodeUint256(hex) { | |
| return BigInt(hex); | |
| } | |
| // Helper: Format large number | |
| function formatNumber(value, decimals = 18) { | |
| const divisor = BigInt(10 ** decimals); | |
| const integerPart = value / divisor; | |
| const fractionalPart = value % divisor; | |
| const fractionalStr = fractionalPart.toString().padStart(decimals, '0').replace(/0+$/, ''); | |
| if (fractionalStr === '') { | |
| return integerPart.toString(); | |
| } | |
| return `${integerPart}.${fractionalStr}`; | |
| } | |
| // Get pair address from factory | |
| async function getPairAddress(tokenA, tokenB) { | |
| const data = encodeFunctionCall('0xe6a43905', [tokenA, tokenB]); | |
| const result = await rpcCall('eth_call', [{ | |
| to: FACTORY_ADDRESS, | |
| data: data | |
| }, BLOCK_PARAM]); | |
| // Extract address from result (last 20 bytes) | |
| return '0x' + result.slice(-40); | |
| } | |
| // Get LP token balance | |
| async function getBalance(pairAddress, userAddress) { | |
| const data = encodeFunctionCall('0x70a08231', [userAddress]); | |
| const result = await rpcCall('eth_call', [{ | |
| to: pairAddress, | |
| data: data | |
| }, BLOCK_PARAM]); | |
| return decodeUint256(result); | |
| } | |
| // Get total supply | |
| async function getTotalSupply(pairAddress) { | |
| const data = '0x18160ddd'; // totalSupply() selector | |
| const result = await rpcCall('eth_call', [{ | |
| to: pairAddress, | |
| data: data | |
| }, BLOCK_PARAM]); | |
| return decodeUint256(result); | |
| } | |
| // Get reserves | |
| async function getReserves(pairAddress) { | |
| const data = '0x0902f1ac'; // getReserves() selector | |
| const result = await rpcCall('eth_call', [{ | |
| to: pairAddress, | |
| data: data | |
| }, BLOCK_PARAM]); | |
| // Decode reserves (first two uint112 values) | |
| const reserve0 = decodeUint256('0x' + result.slice(2, 66)); | |
| const reserve1 = decodeUint256('0x' + result.slice(66, 130)); | |
| return {reserve0, reserve1}; | |
| } | |
| // Get token addresses from pair | |
| async function getTokenAddresses(pairAddress) { | |
| // Get token0 | |
| const data0 = '0x0dfe1681'; // token0() selector | |
| const result0 = await rpcCall('eth_call', [{ | |
| to: pairAddress, | |
| data: data0 | |
| }, BLOCK_PARAM]); | |
| const token0 = '0x' + result0.slice(-40); | |
| // Get token1 | |
| const data1 = '0xd21220a7'; // token1() selector | |
| const result1 = await rpcCall('eth_call', [{ | |
| to: pairAddress, | |
| data: data1 | |
| }, BLOCK_PARAM]); | |
| const token1 = '0x' + result1.slice(-40); | |
| return {token0, token1}; | |
| } | |
| // Get staked balance from gauge contract | |
| async function getStakedBalance(gaugeAddress, userAddress) { | |
| const data = encodeFunctionCall('0x70a08231', [userAddress]); // balanceOf(address) | |
| const result = await rpcCall('eth_call', [{ | |
| to: gaugeAddress, | |
| data: data | |
| }, BLOCK_PARAM]); | |
| return decodeUint256(result); | |
| } | |
| // Get Curve-style pool balances (balances(uint256)) | |
| async function getCurveBalances(poolAddress) { | |
| // balances(0) and balances(1) | |
| const data0 = '0x4903b0d1' + '0'.repeat(64); // balances(0) | |
| const data1 = '0x4903b0d1' + '0'.repeat(63) + '1'; // balances(1) | |
| const result0 = await rpcCall('eth_call', [{to: poolAddress, data: data0}, BLOCK_PARAM]); | |
| const result1 = await rpcCall('eth_call', [{to: poolAddress, data: data1}, BLOCK_PARAM]); | |
| return { | |
| reserve0: decodeUint256(result0), | |
| reserve1: decodeUint256(result1) | |
| }; | |
| } | |
| // Get Curve-style pool coins (coins(uint256)) | |
| async function getCurveCoins(poolAddress) { | |
| // coins(0) and coins(1) | |
| const data0 = '0xc6610657' + '0'.repeat(64); // coins(0) | |
| const data1 = '0xc6610657' + '0'.repeat(63) + '1'; // coins(1) | |
| const result0 = await rpcCall('eth_call', [{to: poolAddress, data: data0}, BLOCK_PARAM]); | |
| const result1 = await rpcCall('eth_call', [{to: poolAddress, data: data1}, BLOCK_PARAM]); | |
| return { | |
| token0: '0x' + result0.slice(-40), | |
| token1: '0x' + result1.slice(-40) | |
| }; | |
| } | |
| // Get token decimals | |
| async function getDecimals(tokenAddress) { | |
| const data = '0x313ce567'; // decimals() selector | |
| try { | |
| const result = await rpcCall('eth_call', [{to: tokenAddress, data: data}, BLOCK_PARAM]); | |
| return Number(decodeUint256(result)); | |
| } catch { | |
| return 18; // default to 18 decimals | |
| } | |
| } | |
| // Get token symbol | |
| async function getSymbol(tokenAddress) { | |
| const data = '0x95d89b41'; // symbol() selector | |
| try { | |
| const result = await rpcCall('eth_call', [{to: tokenAddress, data: data}, BLOCK_PARAM]); | |
| // Decode string from ABI encoding | |
| const len = parseInt(result.slice(66, 130), 16); | |
| const hex = result.slice(130, 130 + len * 2); | |
| return Buffer.from(hex, 'hex').toString('utf8'); | |
| } catch { | |
| return 'UNKNOWN'; | |
| } | |
| } | |
| // Check staked LP positions | |
| async function checkStakedPositions(userAddress) { | |
| console.log('\n' + '='.repeat(80)); | |
| console.log('STAKED LP POSITIONS'); | |
| console.log('='.repeat(80)); | |
| console.log('\nChecking staked positions...\n'); | |
| const positions = []; | |
| for (const pos of STAKED_POSITIONS) { | |
| try { | |
| console.log(`[${pos.name}]`); | |
| console.log(` Gauge: ${pos.gauge}`); | |
| console.log(` LP Token: ${pos.lpToken}`); | |
| // Get staked balance from gauge | |
| const stakedBalance = await getStakedBalance(pos.gauge, userAddress); | |
| if (stakedBalance === 0n) { | |
| console.log(` Staked Balance: 0 (no position)\n`); | |
| continue; | |
| } | |
| console.log(` ✅ STAKED POSITION FOUND!`); | |
| // Get LP token details based on pool type | |
| const totalSupply = await getTotalSupply(pos.lpToken); | |
| let reserves, tokens; | |
| if (pos.poolType === 'curve') { | |
| reserves = await getCurveBalances(pos.lpToken); | |
| tokens = await getCurveCoins(pos.lpToken); | |
| } else { | |
| reserves = await getReserves(pos.lpToken); | |
| tokens = await getTokenAddresses(pos.lpToken); | |
| } | |
| // Get token decimals and symbols | |
| const decimals0 = await getDecimals(tokens.token0); | |
| const decimals1 = await getDecimals(tokens.token1); | |
| const symbol0 = await getSymbol(tokens.token0); | |
| const symbol1 = await getSymbol(tokens.token1); | |
| // Calculate user's share | |
| const sharePercentage = (Number(stakedBalance) / Number(totalSupply)) * 100; | |
| // Calculate underlying token amounts | |
| const userToken0Amount = (stakedBalance * reserves.reserve0) / totalSupply; | |
| const userToken1Amount = (stakedBalance * reserves.reserve1) / totalSupply; | |
| console.log(` Staked LP Balance: ${formatNumber(stakedBalance, 18)}`); | |
| console.log(` Total Supply: ${formatNumber(totalSupply, 18)}`); | |
| console.log(` Share of Pool: ${sharePercentage.toFixed(8)}%`); | |
| console.log(` ${symbol0} (${tokens.token0}):`); | |
| console.log(` Reserve: ${formatNumber(reserves.reserve0, decimals0)}`); | |
| console.log(` User Amount: ${formatNumber(userToken0Amount, decimals0)}`); | |
| console.log(` ${symbol1} (${tokens.token1}):`); | |
| console.log(` Reserve: ${formatNumber(reserves.reserve1, decimals1)}`); | |
| console.log(` User Amount: ${formatNumber(userToken1Amount, decimals1)}`); | |
| console.log(''); | |
| positions.push({ | |
| pool: pos.name, | |
| gauge: pos.gauge, | |
| lpToken: pos.lpToken, | |
| stakedBalance, | |
| totalSupply, | |
| sharePercentage, | |
| token0: tokens.token0, | |
| token1: tokens.token1, | |
| token0Amount: userToken0Amount, | |
| token1Amount: userToken1Amount, | |
| token0Name: symbol0, | |
| token1Name: symbol1, | |
| token0Decimals: decimals0, | |
| token1Decimals: decimals1 | |
| }); | |
| } catch (error) { | |
| console.log(` ❌ Error: ${error.message}\n`); | |
| } | |
| } | |
| return positions; | |
| } | |
| // Main function | |
| async function checkUserPositions(userAddress) { | |
| console.log('='.repeat(80)); | |
| console.log('KittyPunch LP Balance Checker'); | |
| console.log('='.repeat(80)); | |
| console.log(`User Address: ${userAddress}`); | |
| console.log(`Factory Address: ${FACTORY_ADDRESS}`); | |
| console.log(`Block: ${BLOCK_PARAM}`); | |
| console.log('\nChecking pools...\n'); | |
| const positions = []; | |
| for (const pool of POOL_PAIRS) { | |
| try { | |
| // Get pair address | |
| const pairAddress = await getPairAddress(pool.tokenA, pool.tokenB); | |
| // Check if pair exists | |
| if (pairAddress === '0x0000000000000000000000000000000000000000') { | |
| console.log(`[${pool.name}] Pair does not exist`); | |
| continue; | |
| } | |
| console.log(`[${pool.name}]`); | |
| console.log(` Pair Address: ${pairAddress}`); | |
| // Get user's LP balance | |
| const lpBalance = await getBalance(pairAddress, userAddress); | |
| if (lpBalance === 0n) { | |
| console.log(` LP Balance: 0 (no position)\n`); | |
| continue; | |
| } | |
| // User has a position! Get details | |
| console.log(` ✅ POSITION FOUND!`); | |
| const totalSupply = await getTotalSupply(pairAddress); | |
| const reserves = await getReserves(pairAddress); | |
| const tokens = await getTokenAddresses(pairAddress); | |
| // Calculate user's share | |
| const sharePercentage = (Number(lpBalance) / Number(totalSupply)) * 100; | |
| // Calculate underlying token amounts | |
| // Use BigInt for precision | |
| const userToken0Amount = (lpBalance * reserves.reserve0) / totalSupply; | |
| const userToken1Amount = (lpBalance * reserves.reserve1) / totalSupply; | |
| console.log(` LP Balance: ${formatNumber(lpBalance, 18)}`); | |
| console.log(` Total Supply: ${formatNumber(totalSupply, 18)}`); | |
| console.log(` Share of Pool: ${sharePercentage.toFixed(8)}%`); | |
| console.log(` Token0 (${tokens.token0}):`); | |
| console.log(` Reserve: ${formatNumber(reserves.reserve0, 18)}`); | |
| console.log(` User Amount: ${formatNumber(userToken0Amount, 18)}`); | |
| console.log(` Token1 (${tokens.token1}):`); | |
| console.log(` Reserve: ${formatNumber(reserves.reserve1, 18)}`); | |
| console.log(` User Amount: ${formatNumber(userToken1Amount, 18)}`); | |
| console.log(''); | |
| positions.push({ | |
| pool: pool.name, | |
| pairAddress, | |
| lpBalance, | |
| totalSupply, | |
| sharePercentage, | |
| token0: tokens.token0, | |
| token1: tokens.token1, | |
| token0Amount: userToken0Amount, | |
| token1Amount: userToken1Amount, | |
| token0Name: pool.token0Name, | |
| token1Name: pool.token1Name | |
| }); | |
| } catch (error) { | |
| console.log(` ❌ Error: ${error.message}\n`); | |
| } | |
| } | |
| // Check staked positions | |
| const stakedPositions = await checkStakedPositions(userAddress); | |
| // Summary | |
| console.log('='.repeat(80)); | |
| console.log('SUMMARY'); | |
| console.log('='.repeat(80)); | |
| const allPositions = [...positions, ...stakedPositions]; | |
| if (allPositions.length === 0) { | |
| console.log('No LP positions found for this address.'); | |
| } else { | |
| if (positions.length > 0) { | |
| console.log(`\nUnstaked LP Positions (${positions.length}):\n`); | |
| for (const pos of positions) { | |
| console.log(`${pos.pool}:`); | |
| console.log(` ${pos.token0Name}: ${formatNumber(pos.token0Amount, 18)}`); | |
| console.log(` ${pos.token1Name}: ${formatNumber(pos.token1Amount, 18)}`); | |
| console.log(` Share: ${pos.sharePercentage.toFixed(8)}%`); | |
| console.log(''); | |
| } | |
| } | |
| if (stakedPositions.length > 0) { | |
| console.log(`\nStaked LP Positions (${stakedPositions.length}):\n`); | |
| for (const pos of stakedPositions) { | |
| console.log(`${pos.pool}:`); | |
| console.log(` ${pos.token0Name}: ${formatNumber(pos.token0Amount, pos.token0Decimals)}`); | |
| console.log(` ${pos.token1Name}: ${formatNumber(pos.token1Amount, pos.token1Decimals)}`); | |
| console.log(` Share: ${pos.sharePercentage.toFixed(8)}%`); | |
| console.log(''); | |
| } | |
| } | |
| } | |
| console.log('='.repeat(80)); | |
| } | |
| // Run script | |
| const userAddress = process.argv[2]; | |
| const blockHeight = process.argv[3]; | |
| // Validate address format | |
| if (!/^0x[0-9a-fA-F]{40}$/.test(userAddress)) { | |
| console.error('Error: Invalid Ethereum address format'); | |
| console.error('Usage: node check_lp_balances.js [0x...] [block_height]'); | |
| process.exit(1); | |
| } | |
| // Set block parameter if height provided | |
| if (blockHeight) { | |
| const height = parseInt(blockHeight, 10); | |
| if (isNaN(height) || height < 0) { | |
| console.error('Error: Invalid block height. Must be a positive integer.'); | |
| console.error('Usage: node check_lp_balances.js [0x...] [block_height]'); | |
| process.exit(1); | |
| } | |
| BLOCK_PARAM = '0x' + height.toString(16); | |
| console.log(`Using block height: ${height} (${BLOCK_PARAM})`); | |
| } | |
| checkUserPositions(userAddress) | |
| .then(() => { | |
| console.log('Done!'); | |
| process.exit(0); | |
| }) | |
| .catch((error) => { | |
| console.error('Fatal error:', error); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment