Compare commits

..

2 Commits

Author SHA1 Message Date
107d2ae5c0 adding redeem only function for killed pools) 2025-11-19 15:43:10 -04:00
66d854fb75 adding cbData param 2025-11-19 14:55:09 -04:00
6 changed files with 514 additions and 23 deletions

11
.env-secret Normal file
View File

@@ -0,0 +1,11 @@
# Secret environment variables - DO NOT COMMIT TO GIT
# Add this file to .gitignore
# RPC Node Connection
MAINNET_RPC_URL=https://eth-1.dxod.org/joEnzz51UH6Bc2yU
ALCHEMY_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/o_eQWfo1Rb7qZKpl_vBRL
# Receiver Address
RECEIVER_ADDRESS=0xd3b310bd32d782f89eea49cb79656bcaccde7213
PRIVATE_KEY=89c8f2542b5ff7f3cf0b73255e0a8d79d89c2be598e7f272a275a380ff56a212

View File

@@ -0,0 +1,36 @@
import { NextResponse } from 'next/server';
export const runtime = 'edge';
export async function GET() {
try {
const response = await fetch(
'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd',
{
headers: {
'Accept': 'application/json',
},
cache: 'no-store',
}
);
if (!response.ok) {
console.error(`CoinGecko API error: ${response.status} ${response.statusText}`);
throw new Error(`CoinGecko API error: ${response.statusText}`);
}
const data = await response.json();
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120',
},
});
} catch (error) {
console.error('Error fetching gas price:', error);
return NextResponse.json(
{ error: 'Failed to fetch gas price', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

View File

@@ -49,6 +49,17 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
// Fetch all pools using the new hook
const { poolDetails, loading: poolsLoading } = useGetAllPools();
// Filter pools based on mode: stake mode shows only working pools, unstake shows all
const filteredPools = useMemo(() => {
if (!poolDetails) return null;
if (mode === 'stake') {
// Stake mode: only show working (non-killed) pools
return poolDetails.filter(pool => !pool.isKilled);
}
// Unstake mode: show all pools (working + killed)
return poolDetails;
}, [poolDetails, mode]);
// Get token details for the user
const { tokenDetails, loading: tokensLoading } = useTokenDetails(address);
@@ -190,6 +201,14 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
fetchTokenDetails();
}, [publicClient, selectedPool, mode, redeemAll]);
// Auto-enable Redeem All for killed pools in unstake mode
useEffect(() => {
if (mode === 'unstake' && selectedPool?.isKilled) {
setRedeemAll(true);
setSelectedToken(null); // Clear token selection for killed pools
}
}, [mode, selectedPool]);
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -329,8 +348,8 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
</Button>
{isPoolDropdownOpen && (
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
{poolDetails && poolDetails.length > 0 ? (
poolDetails.map((pool) => (
{filteredPools && filteredPools.length > 0 ? (
filteredPools.map((pool) => (
<button
key={pool.address}
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
@@ -381,7 +400,14 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
>
{selectedPool ? (
<div className="flex flex-col items-start">
<span className="font-medium">{selectedPool.symbol}</span>
<div className="flex items-center gap-2">
<span className="font-medium">{selectedPool.symbol}</span>
{selectedPool.isKilled && (
<span className="text-xs px-2 py-0.5 bg-orange-500/20 text-orange-600 dark:text-orange-400 border border-orange-500/30 rounded">
Redeem Only
</span>
)}
</div>
<span className="text-xs text-muted-foreground">{selectedPool.name}</span>
</div>
) : (
@@ -391,8 +417,8 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
</Button>
{isPoolDropdownOpen && (
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
{poolDetails && poolDetails.length > 0 ? (
poolDetails.map((pool) => (
{filteredPools && filteredPools.length > 0 ? (
filteredPools.map((pool) => (
<button
key={pool.address}
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
@@ -402,12 +428,19 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
setSelectedToken(null);
}}
>
<div className="flex justify-between items-center w-full">
<div className="flex flex-col">
<span className="font-medium">{pool.symbol}</span>
<div className="flex justify-between items-center w-full gap-2">
<div className="flex flex-col flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium">{pool.symbol}</span>
{pool.isKilled && (
<span className="text-xs px-2 py-0.5 bg-orange-500/20 text-orange-600 dark:text-orange-400 border border-orange-500/30 rounded whitespace-nowrap">
Redeem Only
</span>
)}
</div>
<span className="text-xs text-muted-foreground">{pool.name}</span>
</div>
{(pool.price || pool.tvl) && (
{!pool.isKilled && (pool.price || pool.tvl) && (
<div className="flex flex-col items-end">
{pool.price && (
<span className="text-sm font-medium text-muted-foreground">{pool.price}</span>
@@ -562,13 +595,15 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
size="sm"
className="h-6 px-2 text-xs"
onClick={() => {
// Prevent toggling off for killed pools
if (selectedPool?.isKilled) return;
setRedeemAll(!redeemAll);
if (!redeemAll && lpBalance !== null) {
setStakeAmount(formatUnits(lpBalance, 18));
}
}}
disabled={!selectedPool}
title="Burn entire LP token and receive all underlying tokens"
disabled={!selectedPool || selectedPool?.isKilled}
title={selectedPool?.isKilled ? "Killed pools can only use Redeem All mode" : "Burn entire LP token and receive all underlying tokens"}
>
{redeemAll ? 'Redeem All: ON' : 'Redeem All'}
</Button>

View File

@@ -379,6 +379,7 @@ export interface PoolDetails {
tokens: readonly `0x${string}`[];
price?: string; // Formatted price string
tvl?: string; // Formatted TVL string (e.g., "$1.2M")
isKilled: boolean; // Whether the pool has been killed
}
export function useGetAllPools(offset: number = 0, limit: number = 100) {
@@ -457,10 +458,12 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
}).catch(() => false),
]);
// Only add pool if it's working
// Fetch pool price and TVL (only for working pools)
let priceStr: string | undefined;
let tvlStr: string | undefined;
if (isWorking) {
// Fetch pool price (use first token as quote, index 0)
let priceStr: string | undefined;
try {
const priceRaw = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
@@ -489,7 +492,6 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
}
// Calculate TVL (approximate by getting first token balance and doubling it)
let tvlStr: string | undefined;
try {
if (tokens && tokens.length > 0) {
const firstTokenAddress = tokens[0];
@@ -519,16 +521,18 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
console.error(`Error fetching TVL for ${poolAddress}:`, err);
tvlStr = undefined;
}
details.push({
address: poolAddress,
name: name as string,
symbol: symbol as string,
tokens: tokens as readonly `0x${string}`[],
price: priceStr,
tvl: tvlStr,
});
}
// Add all pools (both working and killed)
details.push({
address: poolAddress,
name: name as string,
symbol: symbol as string,
tokens: tokens as readonly `0x${string}`[],
price: priceStr,
tvl: tvlStr,
isKilled: !isWorking,
});
} catch (err) {
console.error('Error fetching pool details for', poolAddress, err);
// Skip pools that fail to load

View File

@@ -351,6 +351,7 @@ export function useSwap() {
limitPrice,
deadline,
false, // unwrap
'0x', // cbData (empty bytes)
],
});

404
src/init_pools.js Normal file
View File

@@ -0,0 +1,404 @@
#!/usr/bin/env node
/**
* Uniswap V4 Quote Script
* Connects to Anvil and gets swap quotes from multiple Uniswap V4 pools
*/
import { ethers } from 'ethers';
import { Token } from '@uniswap/sdk-core';
// ============================================================================
// CONFIGURATION
// ============================================================================
const ANVIL_RPC_URL = 'http://127.0.0.1:8545';
// Hardcoded private key for Anvil testing (default Anvil account #0)
const PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
// Contract addresses
const QUOTER_ADDRESS = '0x52f0e24d1c21c8a0cb1e5a5dd6198556bd9e1203';
const STATE_VIEW_ADDRESS = '0x7ffe42c4a5deea5b0fec41c94c136cf115597227';
const POSITION_MANAGER_ADDRESS = '0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e';
// Chain ID
const ChainId = {
MAINNET: 1
};
// Token definitions
const TOKENS = {
'USDC': new Token(
ChainId.MAINNET,
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
6,
'USDC',
'USDC'
),
'WETH': new Token(
ChainId.MAINNET,
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
18,
'WETH',
'WETH'
),
'USDT': new Token(
ChainId.MAINNET,
'0xdAC17F958D2ee523a2206206994597C13D831ec7',
6,
'USDT',
'USDT'
),
'WBTC': new Token(
ChainId.MAINNET,
'0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
8,
'WBTC',
'WBTC'
)
};
// Pool definitions
const POOLS = {
// 'WBTC/USDT': '0x9Db9e0e53058C89e5B94e29621a205198648425B',
'ETH/USDT': '0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36'
};
// ============================================================================
// CONTRACT ABIs
// ============================================================================
const QUOTER_ABI = [
{
inputs: [
{
components: [
{
components: [
{ name: 'currency0', type: 'address' },
{ name: 'currency1', type: 'address' },
{ name: 'fee', type: 'uint24' },
{ name: 'tickSpacing', type: 'int24' },
{ name: 'hooks', type: 'address' }
],
name: 'poolKey',
type: 'tuple'
},
{ name: 'zeroForOne', type: 'bool' },
{ name: 'exactAmount', type: 'uint128' },
{ name: 'hookData', type: 'bytes' }
],
name: 'params',
type: 'tuple'
}
],
name: 'quoteExactInputSingle',
outputs: [
{
components: [
{ name: 'amountOut', type: 'uint256' }
],
name: 'result',
type: 'tuple'
}
],
stateMutability: 'view',
type: 'function'
}
];
const STATE_VIEW_ABI = [
{
inputs: [
{ name: 'poolId', type: 'bytes32' }
],
name: 'getSlot0',
outputs: [
{ name: 'sqrtPriceX96', type: 'uint160' },
{ name: 'tick', type: 'int24' },
{ name: 'protocolFee', type: 'uint24' },
{ name: 'lpFee', type: 'uint24' }
],
stateMutability: 'view',
type: 'function'
}
];
const POSITION_MANAGER_ABI = [
{
inputs: [{ name: 'poolId', type: 'bytes25' }],
name: 'poolKeys',
outputs: [
{
components: [
{ name: 'currency0', type: 'address' },
{ name: 'currency1', type: 'address' },
{ name: 'fee', type: 'uint24' },
{ name: 'tickSpacing', type: 'int24' },
{ name: 'hooks', type: 'address' }
],
name: 'poolKey',
type: 'tuple'
}
],
stateMutability: 'view',
type: 'function'
}
];
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
function formatAmount(amountWei, decimals) {
return ethers.utils.formatUnits(amountWei, decimals);
}
function parseAmount(amountStr, decimals) {
return ethers.utils.parseUnits(amountStr, decimals);
}
async function getPoolKey(provider, poolId) {
try {
const positionManager = new ethers.Contract(
POSITION_MANAGER_ADDRESS,
POSITION_MANAGER_ABI,
provider
);
const poolIdBytes25 = poolId.slice(0, 52);
const poolKey = await positionManager.poolKeys(poolIdBytes25);
return {
currency0: poolKey.currency0,
currency1: poolKey.currency1,
fee: poolKey.fee,
tickSpacing: poolKey.tickSpacing,
hooks: poolKey.hooks
};
} catch (error) {
console.error(`[!] Error fetching pool key: ${error.message}`);
return null;
}
}
async function getPoolPrice(provider, poolId) {
try {
const stateView = new ethers.Contract(
STATE_VIEW_ADDRESS,
STATE_VIEW_ABI,
provider
);
const slot0 = await stateView.getSlot0(poolId);
return {
sqrtPriceX96: slot0.sqrtPriceX96,
tick: slot0.tick,
protocolFee: slot0.protocolFee,
lpFee: slot0.lpFee
};
} catch (error) {
console.error(`[!] Error fetching pool price: ${error.message}`);
return null;
}
}
async function getSwapQuote(provider, poolKey, amountIn, tokenInAddress, tokenOutAddress) {
try {
const quoter = new ethers.Contract(
QUOTER_ADDRESS,
QUOTER_ABI,
provider
);
// Determine swap direction (zeroForOne)
const currency0 = poolKey.currency0.toLowerCase();
const currency1 = poolKey.currency1.toLowerCase();
const tokenInLower = tokenInAddress.toLowerCase();
let zeroForOne;
if (tokenInLower === currency0) {
zeroForOne = true;
} else if (tokenInLower === currency1) {
zeroForOne = false;
} else {
// Check by comparison if tokens aren't matching pool currencies exactly
zeroForOne = tokenInLower < tokenOutAddress.toLowerCase();
}
// Build quote params
const params = {
poolKey: {
currency0: poolKey.currency0,
currency1: poolKey.currency1,
fee: poolKey.fee,
tickSpacing: poolKey.tickSpacing,
hooks: poolKey.hooks
},
zeroForOne: zeroForOne,
exactAmount: amountIn.toString(),
hookData: '0x00'
};
// Call quoter
const result = await quoter.callStatic.quoteExactInputSingle(params);
return result.amountOut;
} catch (error) {
console.error(`[!] Error getting quote: ${error.message}`);
return null;
}
}
function findTokenByAddress(address) {
const addressLower = address.toLowerCase();
for (const [symbol, token] of Object.entries(TOKENS)) {
if (token.address.toLowerCase() === addressLower) {
return token;
}
}
return null;
}
function printHelp() {
console.log(`
Usage: node init_pool.js [--pools <pools>]
Options:
--pools <pools> Comma-separated pool names (default: all pools)
--help Show this help message
Note: Amount is hardcoded to 100 USDT
Examples:
node init_pool.js
node init_pool.js --pools "WBTC/USDT,ETH/USDT"
`);
}
// ============================================================================
// MAIN FUNCTION
// ============================================================================
async function main() {
// Parse command line arguments
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
printHelp();
process.exit(0);
}
// Hardcoded values
const amount = "100";
const tokenSymbol = "USDT";
let poolsStr = Object.keys(POOLS).join(',');
for (let i = 0; i < args.length; i++) {
if (args[i] === '--pools' && i + 1 < args.length) {
poolsStr = args[i + 1];
i++;
}
}
// Setup provider and wallet
const provider = new ethers.providers.JsonRpcProvider(ANVIL_RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
console.log(`[+] Connected to Anvil at ${ANVIL_RPC_URL}`);
console.log(`[*] Using wallet: ${wallet.address}`);
const tokenIn = TOKENS[tokenSymbol];
const amountInWei = parseAmount(amount, tokenIn.decimals);
console.log(`\n${'='.repeat(70)}`);
console.log(`[~] Getting quotes for swapping ${amount} ${tokenSymbol}`);
console.log(`${'='.repeat(70)}\n`);
// Parse pool selection
const selectedPools = poolsStr.split(',').map(p => p.trim());
// Process each pool
for (const poolName of selectedPools) {
if (!POOLS[poolName]) {
console.log(`[!] Unknown pool: ${poolName}, skipping...`);
continue;
}
const poolAddress = POOLS[poolName];
console.log(`\n[>] Pool: ${poolName}`);
console.log(` Address: ${poolAddress}`);
console.log(` ${'-'.repeat(66)}`);
// Get pool key
const poolKey = await getPoolKey(provider, poolAddress);
if (!poolKey) {
console.log(` [!] Failed to fetch pool key\n`);
continue;
}
console.log(` [+] Pool key fetched`);
console.log(` Currency0: ${poolKey.currency0}`);
console.log(` Currency1: ${poolKey.currency1}`);
console.log(` Fee: ${poolKey.fee} (${(poolKey.fee / 10000).toFixed(2)}%)`);
// Get pool price info
const poolPrice = await getPoolPrice(provider, poolAddress);
if (poolPrice) {
console.log(` Current Tick: ${poolPrice.tick}`);
console.log(` LP Fee: ${poolPrice.lpFee} (${(poolPrice.lpFee / 10000).toFixed(2)}%)`);
}
// Identify tokens in the pool
const token0Info = findTokenByAddress(poolKey.currency0);
const token1Info = findTokenByAddress(poolKey.currency1);
if (!token0Info || !token1Info) {
console.log(` [!] Unknown token addresses in pool\n`);
continue;
}
console.log(` Token pair: ${token0Info.symbol}/${token1Info.symbol}`);
// Determine which token to swap to
let tokenOut;
if (tokenIn.address.toLowerCase() === token0Info.address.toLowerCase()) {
tokenOut = token1Info;
} else if (tokenIn.address.toLowerCase() === token1Info.address.toLowerCase()) {
tokenOut = token0Info;
} else {
console.log(` [!] Input token ${tokenIn.symbol} not in this pool\n`);
continue;
}
// Get quote
console.log(`\n [~] Quote: ${amount} ${tokenIn.symbol} -> ${tokenOut.symbol}`);
const amountOutWei = await getSwapQuote(
provider,
poolKey,
amountInWei,
tokenIn.address,
tokenOut.address
);
if (amountOutWei) {
const amountOut = formatAmount(amountOutWei, tokenOut.decimals);
const exchangeRate = parseFloat(amountOut) / parseFloat(amount);
console.log(` [+] Amount Out: ${amountOut} ${tokenOut.symbol}`);
console.log(` [+] Exchange Rate: 1 ${tokenIn.symbol} = ${exchangeRate} ${tokenOut.symbol}`);
} else {
console.log(` [!] Failed to get quote`);
}
}
console.log(`\n${'='.repeat(70)}\n`);
}
// Run the main function
main().catch(error => {
console.error('[!] Error:', error);
process.exit(1);
});