Compare commits

...

7 Commits

8 changed files with 432 additions and 138 deletions

View File

@@ -1,11 +0,0 @@
# 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

3
.gitignore vendored
View File

@@ -5,6 +5,9 @@
/.pnp
.pnp.js
*secret*
.env
# testing
/coverage

View File

@@ -8,6 +8,7 @@
"create-pool": "node create_pool_from_prices.js"
},
"dependencies": {
"dotenv": "^17.2.3",
"ethers": "^5.7.2"
}
}

View File

@@ -224,7 +224,7 @@ export default function AboutPage() {
<p className="text-muted-foreground leading-relaxed">
Verify our contracts on{' '}
<a
href="https://sepolia.etherscan.io/address/0x081aA8AB1984680087c01a5Cd50fC9f49742434D#code"
href="https://etherscan.io/address/0x42977f565971F6D288a05ddEbC87A17276F71A29#code"
target="liqp_etherscan"
rel="noopener noreferrer"
className="text-primary hover:underline"

View File

@@ -6,17 +6,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { ArrowDownUp, ChevronDown, Settings, CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { useAccount } from 'wagmi';
import { useAccount, useChainId } from 'wagmi';
import { useTokenDetails, useGetPoolsByToken, type TokenDetails } from '@/hooks/usePartyPlanner';
import { useSwapAmounts, useSwap, selectBestSwapRoute, type ActualSwapAmounts } from '@/hooks/usePartyPool';
import { formatUnits, parseUnits } from 'viem';
import { SwapReviewModal } from './swap-review-modal';
import UniswapQuote from './uniswap-quote';
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
export function SwapForm() {
const { t } = useTranslation();
const { isConnected, address } = useAccount();
const chainId = useChainId();
const [fromAmount, setFromAmount] = useState('');
const [toAmount, setToAmount] = useState('');
const [selectedFromToken, setSelectedFromToken] = useState<TokenDetails | null>(null);
@@ -404,6 +406,19 @@ export function SwapForm() {
</div>
)}
{/*/!* Uniswap Quote - Hidden (under construction) *!/*/}
{/*{false && fromAmount && selectedFromToken && selectedToToken && (*/}
{/* <UniswapQuote*/}
{/* amountIn={fromAmount}*/}
{/* tokenInAddress={selectedFromToken.address}*/}
{/* tokenOutAddress={selectedToToken.address}*/}
{/* tokenInDecimals={selectedFromToken.decimals}*/}
{/* tokenOutDecimals={selectedToToken.decimals}*/}
{/* tokenOutSymbol={selectedToToken.symbol}*/}
{/* chainId={chainId || 1}*/}
{/* />*/}
{/*)}*/}
{/* Gas Estimate, Slippage, and Fees */}
{isConnected && fromAmount && toAmount && (
<div className="px-4 py-2 bg-muted/30 rounded-lg space-y-2">

View File

@@ -0,0 +1,248 @@
'use client';
import { useState, useEffect } from 'react';
import { parseUnits, formatUnits } from 'viem';
interface UniswapQuoteProps {
amountIn: string;
tokenInAddress: string | null;
tokenOutAddress: string | null;
tokenInDecimals: number;
tokenOutDecimals: number;
tokenOutSymbol: string;
chainId: number;
}
interface TokenPrices {
[key: string]: number;
}
export default function UniswapQuote({
amountIn,
tokenInAddress,
tokenOutAddress,
tokenInDecimals,
tokenOutDecimals,
tokenOutSymbol,
chainId
}: UniswapQuoteProps) {
const [quote, setQuote] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [prices, setPrices] = useState<TokenPrices | null>(null);
// Only show on mainnet
if (chainId !== 1) {
return null;
}
// Don't fetch quote if tokens aren't selected
if (!tokenInAddress || !tokenOutAddress) {
return null;
}
// Fetch token prices from CoinGecko using contract addresses
useEffect(() => {
if (!tokenInAddress || !tokenOutAddress) return;
const fetchPrices = async () => {
try {
// Fetch both token prices separately using the correct endpoint
const [tokenInResponse, tokenOutResponse] = await Promise.all([
fetch(`https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=${tokenInAddress}&vs_currencies=usd`),
fetch(`https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=${tokenOutAddress}&vs_currencies=usd`)
]);
const tokenInData = await tokenInResponse.json();
const tokenOutData = await tokenOutResponse.json();
const tokenInPrice = tokenInData[tokenInAddress.toLowerCase()]?.usd || 0;
const tokenOutPrice = tokenOutData[tokenOutAddress.toLowerCase()]?.usd || 0;
setPrices({
tokenIn: tokenInPrice,
tokenOut: tokenOutPrice
});
console.log('Token prices:', { tokenInPrice, tokenOutPrice, tokenInData, tokenOutData });
} catch (err) {
console.error('Failed to fetch prices:', err);
}
};
fetchPrices();
// Refresh prices every 30 seconds
const interval = setInterval(fetchPrices, 30000);
return () => clearInterval(interval);
}, [tokenInAddress, tokenOutAddress]);
const getQuote = async () => {
if (!amountIn || parseFloat(amountIn) <= 0) {
setError('Please enter a valid amount');
return;
}
setLoading(true);
setError('');
try {
// Convert amount to smallest unit based on token decimals using viem
const amountInSmallestUnit = parseUnits(amountIn, tokenInDecimals).toString();
const response = await fetch('https://api.uniswap.org/v2/quote', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': 'https://app.uniswap.org'
},
body: JSON.stringify({
amount: amountInSmallestUnit,
tokenIn: tokenInAddress,
tokenInChainId: chainId,
tokenOut: tokenOutAddress,
tokenOutChainId: chainId,
type: 'EXACT_INPUT',
configs: [
{
protocols: ['V2', 'V3', 'V4'],
enableUniversalRouter: true,
routingType: 'CLASSIC'
}
]
})
});
if (!response.ok) throw new Error('Failed to fetch quote');
const data = await response.json();
console.log('Uniswap Quote:', data);
setQuote(data);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
const formatTokenAmount = (amount: string) => {
const formatted = formatUnits(BigInt(amount), tokenOutDecimals);
return parseFloat(formatted).toLocaleString('en-US', {
maximumFractionDigits: tokenOutDecimals >= 18 ? 0 : 6
});
};
const getQuoteAmount = () => {
if (!quote) return '0';
// Handle nested quote structure
return quote.quote?.quote || quote.quote || '0';
};
const calculateCostBreakdown = () => {
if (!quote || !prices || !prices.tokenIn) return null;
const tokenAmount = parseFloat(amountIn);
const tradeValueUSD = tokenAmount * prices.tokenIn;
// Access nested quote object
const quoteData = quote.quote || quote;
// 1. Gas Cost
const gasCostUSD = parseFloat(quoteData.gasUseEstimateUSD || '0');
// 2. Uniswap UX Fee (0.25%)
const uniswapFeePercent = 0.25;
const uniswapFeeUSD = (uniswapFeePercent / 100) * tradeValueUSD;
const totalCostUSD = gasCostUSD + uniswapFeeUSD;
console.log('Cost breakdown calc:', {
gasCostUSD,
uniswapFeeUSD,
totalCostUSD,
tradeValueUSD,
quoteData
});
return {
gasCostUSD,
uniswapFeePercent,
uniswapFeeUSD,
totalCostUSD,
tradeValueUSD
};
};
const costBreakdown = calculateCostBreakdown();
return (
<div className="w-full max-w-md p-4">
<button
onClick={getQuote}
disabled={loading}
className="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded"
>
{loading ? 'Getting Quote...' : 'Get Quote'}
</button>
{error && (
<div className="mt-3 p-3 bg-red-50 text-red-700 rounded text-sm">
{error}
</div>
)}
{quote && costBreakdown && (
<div className="mt-4 space-y-3">
<div className="p-3 bg-gray-50 rounded">
<div className="text-sm text-gray-600">You Get ({tokenOutSymbol})</div>
<div className="text-xl font-bold">
{formatTokenAmount(getQuoteAmount())}
</div>
</div>
{/* Total Costs Breakdown */}
<div className="p-4 bg-blue-50 rounded-lg space-y-3">
<h3 className="font-semibold text-lg text-gray-800">Total Costs Breakdown</h3>
{/* 1. Gas Cost */}
<div className="space-y-1">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700">1. Gas Cost (Network Fee)</span>
<span className="text-sm font-bold text-gray-900">
${costBreakdown.gasCostUSD.toFixed(2)} USD
</span>
</div>
</div>
{/* 2. Uniswap UX Fee */}
<div className="space-y-1">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700">2. Uniswap UX Fee</span>
<span className="text-sm font-bold text-gray-900">
{costBreakdown.uniswapFeePercent}%
</span>
</div>
<div className="text-xs text-gray-600 pl-4">
${costBreakdown.uniswapFeeUSD.toFixed(2)} USD
</div>
</div>
{/* Total */}
<div className="pt-2 border-t border-gray-300">
<div className="flex justify-between items-center">
<span className="text-sm font-bold text-gray-800">Total Estimated Cost</span>
<span className="text-base font-bold text-red-600">
${costBreakdown.totalCostUSD.toFixed(2)} USD
</span>
</div>
</div>
{/* Trade Value Reference */}
<div className="text-xs text-gray-500 text-center pt-1">
Trade Value: ${costBreakdown.tradeValueUSD.toFixed(2)} USD
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -198,88 +198,131 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs
return;
}
// First, fetch all tokens from all working pools
const poolTokensContracts = workingPools.map(poolAddress => ({
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'allTokens',
}));
const poolTokensResults = await publicClient.multicall({
contracts: poolTokensContracts as any,
allowFailure: true,
});
// Build a flat list of all unique token addresses we need to query
const uniqueTokenAddresses = new Set<`0x${string}`>();
uniqueTokenAddresses.add(tokenAddress); // Add input token
poolTokensResults.forEach((result) => {
if (result.status === 'success') {
const tokens = result.result as readonly `0x${string}`[];
tokens.forEach(token => uniqueTokenAddresses.add(token));
}
});
const tokenAddressesArray = Array.from(uniqueTokenAddresses);
// Build multicall for all token symbols and decimals
const tokenDataContracts = tokenAddressesArray.flatMap(addr => [
{
address: addr,
abi: ERC20ABI,
functionName: 'symbol',
},
{
address: addr,
abi: ERC20ABI,
functionName: 'decimals',
},
]);
const tokenDataResults = await publicClient.multicall({
contracts: tokenDataContracts as any,
allowFailure: true,
});
// Parse token data into a map
const tokenDataMap = new Map<string, { symbol: string | null; decimals: number | null }>();
for (let i = 0; i < tokenAddressesArray.length; i++) {
const symbolResult = tokenDataResults[i * 2];
const decimalsResult = tokenDataResults[i * 2 + 1];
tokenDataMap.set(tokenAddressesArray[i].toLowerCase(), {
symbol: symbolResult.status === 'success' ? (symbolResult.result as string) : null,
decimals: decimalsResult.status === 'success' ? Number(decimalsResult.result) : null,
});
}
// Map to store available tokens with their swap routes
const tokenRoutesMap = new Map<string, AvailableToken>();
// For each working pool, fetch all tokens and track indices
for (const poolAddress of workingPools) {
try {
const tokensInPool = await publicClient.readContract({
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'allTokens',
}) as readonly `0x${string}`[];
// For each working pool, process tokens
for (let poolIdx = 0; poolIdx < workingPools.length; poolIdx++) {
const poolAddress = workingPools[poolIdx];
const poolTokensResult = poolTokensResults[poolIdx];
// Find the input token index in this pool
const inputTokenIndex = tokensInPool.findIndex(
(token) => token.toLowerCase() === tokenAddress.toLowerCase()
);
if (poolTokensResult.status !== 'success') {
console.error('Failed to fetch tokens for pool', poolAddress);
continue;
}
if (inputTokenIndex === -1) {
console.error('Input token not found in pool', poolAddress);
const tokensInPool = poolTokensResult.result as readonly `0x${string}`[];
// Find the input token index in this pool
const inputTokenIndex = tokensInPool.findIndex(
(token) => token.toLowerCase() === tokenAddress.toLowerCase()
);
if (inputTokenIndex === -1) {
console.error('Input token not found in pool', poolAddress);
continue;
}
const inputTokenData = tokenDataMap.get(tokenAddress.toLowerCase());
const inputTokenDecimal = inputTokenData?.decimals ?? null;
// Process each token in the pool
for (let outputTokenIndex = 0; outputTokenIndex < tokensInPool.length; outputTokenIndex++) {
const outputTokenAddress = tokensInPool[outputTokenIndex];
// Skip if it's the same as the input token
if (outputTokenIndex === inputTokenIndex) {
continue;
}
// Process each token in the pool
for (let outputTokenIndex = 0; outputTokenIndex < tokensInPool.length; outputTokenIndex++) {
const outputTokenAddress = tokensInPool[outputTokenIndex];
const outputTokenData = tokenDataMap.get(outputTokenAddress.toLowerCase());
const outputTokenSymbol = outputTokenData?.symbol ?? null;
const outputTokenDecimal = outputTokenData?.decimals ?? null;
// Skip if it's the same as the input token
if (outputTokenIndex === inputTokenIndex) {
continue;
}
// Skip tokens with the same symbol as the selected token
if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) {
continue;
}
// Get the symbol of this token
const outputTokenSymbol = await publicClient.readContract({
// Skip tokens if decimals failed to load
if (inputTokenDecimal === null || outputTokenDecimal === null) {
console.error(`Failed to load decimals for token ${outputTokenAddress} or ${tokenAddress}`);
continue;
}
// Create or update the available token entry
const tokenKey = outputTokenAddress.toLowerCase();
if (!tokenRoutesMap.has(tokenKey)) {
tokenRoutesMap.set(tokenKey, {
address: outputTokenAddress,
abi: ERC20ABI,
functionName: 'symbol',
}).catch(() => null);
const inputTokenDecimal = await publicClient.readContract({
address: tokenAddress,
abi: ERC20ABI,
functionName: 'decimals',
}).catch(() => null);
const outputTokenDecimal = await publicClient.readContract({
address: outputTokenAddress,
abi: ERC20ABI,
functionName: 'decimals',
}).catch(() => null);
// Skip tokens with the same symbol as the selected token
if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) {
continue;
}
// Skip tokens if decimals failed to load
if (inputTokenDecimal === null || outputTokenDecimal === null) {
console.error(`Failed to load decimals for token ${outputTokenAddress} or ${tokenAddress}`);
continue;
}
// Create or update the available token entry
const tokenKey = outputTokenAddress.toLowerCase();
if (!tokenRoutesMap.has(tokenKey)) {
tokenRoutesMap.set(tokenKey, {
address: outputTokenAddress,
symbol: outputTokenSymbol,
swapRoutes: [],
});
}
// Add this swap route
tokenRoutesMap.get(tokenKey)!.swapRoutes.push({
poolAddress,
inputTokenIndex,
outputTokenIndex,
inputTokenDecimal,
outputTokenDecimal,
symbol: outputTokenSymbol,
swapRoutes: [],
});
}
} catch (err) {
console.error('Error fetching tokens from pool', poolAddress, err);
// Add this swap route
tokenRoutesMap.get(tokenKey)!.swapRoutes.push({
poolAddress,
inputTokenIndex,
outputTokenIndex,
inputTokenDecimal,
outputTokenDecimal,
});
}
}
@@ -326,55 +369,54 @@ export function useTokenDetails(userAddress: `0x${string}` | undefined) {
return;
}
// Build multicall contracts array - 4 calls per token (name, symbol, decimals, balanceOf)
const contracts = tokens.flatMap((tokenAddress) => [
{
address: tokenAddress,
abi: ERC20ABI,
functionName: 'name',
},
{
address: tokenAddress,
abi: ERC20ABI,
functionName: 'symbol',
},
{
address: tokenAddress,
abi: ERC20ABI,
functionName: 'decimals',
},
{
address: tokenAddress,
abi: ERC20ABI,
functionName: 'balanceOf',
args: [userAddress],
},
]);
// Execute multicall
const results = await publicClient.multicall({
contracts: contracts as any,
allowFailure: true,
});
// Parse results
const details: TokenDetails[] = [];
// Make individual calls for each token
for (let i = 0; i < tokens.length; i++) {
const tokenAddress = tokens[i];
try {
const [name, symbol, decimals, balance] = await Promise.all([
publicClient.readContract({
address: tokenAddress,
abi: ERC20ABI,
functionName: 'name',
}).catch(() => 'Unknown'),
publicClient.readContract({
address: tokenAddress,
abi: ERC20ABI,
functionName: 'symbol',
}).catch(() => '???'),
publicClient.readContract({
address: tokenAddress,
abi: ERC20ABI,
functionName: 'decimals',
}).catch(() => 18),
publicClient.readContract({
address: tokenAddress,
abi: ERC20ABI,
functionName: 'balanceOf',
args: [userAddress],
}).catch(() => BigInt(0)),
]);
const baseIndex = i * 4;
const nameResult = results[baseIndex];
const symbolResult = results[baseIndex + 1];
const decimalsResult = results[baseIndex + 2];
const balanceResult = results[baseIndex + 3];
details.push({
address: tokenAddress,
name: name as string,
symbol: symbol as string,
decimals: Number(decimals),
balance: balance as bigint,
index: i,
});
} catch (err) {
// Add token with fallback values if individual call fails
details.push({
address: tokenAddress,
name: 'Unknown',
symbol: '???',
decimals: 18,
balance: BigInt(0),
index: i,
});
}
details.push({
address: tokens[i],
name: nameResult.status === 'success' ? (nameResult.result as string) : 'Unknown',
symbol: symbolResult.status === 'success' ? (symbolResult.result as string) : '???',
decimals: decimalsResult.status === 'success' ? Number(decimalsResult.result) : 18,
balance: balanceResult.status === 'success' ? (balanceResult.result as bigint) : BigInt(0),
index: i,
});
}
setTokenDetails(details);
@@ -525,11 +567,9 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
priceStr = `$${finalPrice.toFixed(4)}`;
}
// Calculate TVL (approximate by getting first token balance and multiplying by 3)
// Calculate TVL (approximate by getting first token balance and multiplying by number of tokens)
const tokenBalance = Number(balance) / Math.pow(10, decimals);
console.log('tokenBalance', tokenBalance);
const approximateTVL = tokenBalance * 3;
const approximateTVL = tokenBalance * tokens.length;
tvlStr = formatTVL(approximateTVL);
}
} catch (err) {

View File

@@ -360,9 +360,7 @@ export function useSwap() {
await publicClient.waitForTransactionReceipt({ hash: approvalHash });
console.log('✅ Approval confirmed');
// STEP 2: Calculate limit price and deadline
const slippageBasisPoints = BigInt(Math.floor(slippagePercent * 100));
const limitPrice = (Q96 * (10000n + slippageBasisPoints)) / 10000n;
// STEP 2: Calculate deadline
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200);
console.log('🚀 Executing swap with params:', {
@@ -372,12 +370,12 @@ export function useSwap() {
inputTokenIndex,
outputTokenIndex,
maxAmountIn: maxAmountIn.toString(),
limitPrice: limitPrice.toString(),
limitPrice: '0 (no limit)',
deadline: deadline.toString(),
unwrap: false,
});
// STEP 3: Execute the swap transaction
// STEP 3: Execute the swap transaction with no limit price
const hash = await walletClient.writeContract({
address: poolAddress,
abi: IPartyPoolABI,
@@ -389,7 +387,7 @@ export function useSwap() {
BigInt(inputTokenIndex),
BigInt(outputTokenIndex),
maxAmountIn,
limitPrice,
0n, // no limit price
deadline,
false, // unwrap
'0x', // cbData (empty bytes)