Compare commits
2 Commits
d209742127
...
107d2ae5c0
| Author | SHA1 | Date | |
|---|---|---|---|
| 107d2ae5c0 | |||
| 66d854fb75 |
11
.env-secret
Normal file
11
.env-secret
Normal 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
|
||||||
36
src/app/api/gas-price/route.ts
Normal file
36
src/app/api/gas-price/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,17 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
|
|||||||
// Fetch all pools using the new hook
|
// Fetch all pools using the new hook
|
||||||
const { poolDetails, loading: poolsLoading } = useGetAllPools();
|
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
|
// Get token details for the user
|
||||||
const { tokenDetails, loading: tokensLoading } = useTokenDetails(address);
|
const { tokenDetails, loading: tokensLoading } = useTokenDetails(address);
|
||||||
|
|
||||||
@@ -190,6 +201,14 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
|
|||||||
fetchTokenDetails();
|
fetchTokenDetails();
|
||||||
}, [publicClient, selectedPool, mode, redeemAll]);
|
}, [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
|
// Close dropdowns when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@@ -329,8 +348,8 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
{isPoolDropdownOpen && (
|
{isPoolDropdownOpen && (
|
||||||
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
<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 ? (
|
{filteredPools && filteredPools.length > 0 ? (
|
||||||
poolDetails.map((pool) => (
|
filteredPools.map((pool) => (
|
||||||
<button
|
<button
|
||||||
key={pool.address}
|
key={pool.address}
|
||||||
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
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 ? (
|
{selectedPool ? (
|
||||||
<div className="flex flex-col items-start">
|
<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>
|
<span className="text-xs text-muted-foreground">{selectedPool.name}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -391,8 +417,8 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
{isPoolDropdownOpen && (
|
{isPoolDropdownOpen && (
|
||||||
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
<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 ? (
|
{filteredPools && filteredPools.length > 0 ? (
|
||||||
poolDetails.map((pool) => (
|
filteredPools.map((pool) => (
|
||||||
<button
|
<button
|
||||||
key={pool.address}
|
key={pool.address}
|
||||||
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
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);
|
setSelectedToken(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center w-full">
|
<div className="flex justify-between items-center w-full gap-2">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
<span className="font-medium">{pool.symbol}</span>
|
<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>
|
<span className="text-xs text-muted-foreground">{pool.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{(pool.price || pool.tvl) && (
|
{!pool.isKilled && (pool.price || pool.tvl) && (
|
||||||
<div className="flex flex-col items-end">
|
<div className="flex flex-col items-end">
|
||||||
{pool.price && (
|
{pool.price && (
|
||||||
<span className="text-sm font-medium text-muted-foreground">{pool.price}</span>
|
<span className="text-sm font-medium text-muted-foreground">{pool.price}</span>
|
||||||
@@ -562,13 +595,15 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 px-2 text-xs"
|
className="h-6 px-2 text-xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// Prevent toggling off for killed pools
|
||||||
|
if (selectedPool?.isKilled) return;
|
||||||
setRedeemAll(!redeemAll);
|
setRedeemAll(!redeemAll);
|
||||||
if (!redeemAll && lpBalance !== null) {
|
if (!redeemAll && lpBalance !== null) {
|
||||||
setStakeAmount(formatUnits(lpBalance, 18));
|
setStakeAmount(formatUnits(lpBalance, 18));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!selectedPool}
|
disabled={!selectedPool || selectedPool?.isKilled}
|
||||||
title="Burn entire LP token and receive all underlying tokens"
|
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'}
|
{redeemAll ? 'Redeem All: ON' : 'Redeem All'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -379,6 +379,7 @@ export interface PoolDetails {
|
|||||||
tokens: readonly `0x${string}`[];
|
tokens: readonly `0x${string}`[];
|
||||||
price?: string; // Formatted price string
|
price?: string; // Formatted price string
|
||||||
tvl?: string; // Formatted TVL string (e.g., "$1.2M")
|
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) {
|
export function useGetAllPools(offset: number = 0, limit: number = 100) {
|
||||||
@@ -457,10 +458,12 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
|
|||||||
}).catch(() => false),
|
}).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) {
|
if (isWorking) {
|
||||||
// Fetch pool price (use first token as quote, index 0)
|
// Fetch pool price (use first token as quote, index 0)
|
||||||
let priceStr: string | undefined;
|
|
||||||
try {
|
try {
|
||||||
const priceRaw = await publicClient.readContract({
|
const priceRaw = await publicClient.readContract({
|
||||||
address: partyInfoAddress as `0x${string}`,
|
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)
|
// Calculate TVL (approximate by getting first token balance and doubling it)
|
||||||
let tvlStr: string | undefined;
|
|
||||||
try {
|
try {
|
||||||
if (tokens && tokens.length > 0) {
|
if (tokens && tokens.length > 0) {
|
||||||
const firstTokenAddress = tokens[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);
|
console.error(`Error fetching TVL for ${poolAddress}:`, err);
|
||||||
tvlStr = undefined;
|
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) {
|
} catch (err) {
|
||||||
console.error('Error fetching pool details for', poolAddress, err);
|
console.error('Error fetching pool details for', poolAddress, err);
|
||||||
// Skip pools that fail to load
|
// Skip pools that fail to load
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ export function useSwap() {
|
|||||||
limitPrice,
|
limitPrice,
|
||||||
deadline,
|
deadline,
|
||||||
false, // unwrap
|
false, // unwrap
|
||||||
|
'0x', // cbData (empty bytes)
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
404
src/init_pools.js
Normal file
404
src/init_pools.js
Normal 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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user