Skip to content

Instantly share code, notes, and snippets.

@zhangchiqing
Created March 4, 2026 01:01
Show Gist options
  • Select an option

  • Save zhangchiqing/e868003ce3745b9f42d546d915cf8129 to your computer and use it in GitHub Desktop.

Select an option

Save zhangchiqing/e868003ce3745b9f42d546d915cf8129 to your computer and use it in GitHub Desktop.
check_lp_balance
#!/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