Compare commits

...

15 Commits

14 changed files with 728 additions and 404 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

80
package-lock.json generated
View File

@@ -17,7 +17,7 @@
"clsx": "^2.1.1",
"i18next": "^23.15.0",
"lucide-react": "^0.460.0",
"next": "^15.1.3",
"next": "15.5.7",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -1405,15 +1405,15 @@
}
},
"node_modules/@next/env": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz",
"integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz",
"integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz",
"integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
"cpu": [
"arm64"
],
@@ -1427,9 +1427,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz",
"integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
"cpu": [
"x64"
],
@@ -1443,9 +1443,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz",
"integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
"cpu": [
"arm64"
],
@@ -1459,9 +1459,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz",
"integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
"cpu": [
"arm64"
],
@@ -1475,9 +1475,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz",
"integrity": "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
"cpu": [
"x64"
],
@@ -1491,9 +1491,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz",
"integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
"cpu": [
"x64"
],
@@ -1507,9 +1507,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz",
"integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
"cpu": [
"arm64"
],
@@ -1523,9 +1523,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz",
"integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
"cpu": [
"x64"
],
@@ -6179,12 +6179,12 @@
}
},
"node_modules/next": {
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz",
"integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz",
"integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
"license": "MIT",
"dependencies": {
"@next/env": "15.5.4",
"@next/env": "15.5.7",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -6197,14 +6197,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.4",
"@next/swc-darwin-x64": "15.5.4",
"@next/swc-linux-arm64-gnu": "15.5.4",
"@next/swc-linux-arm64-musl": "15.5.4",
"@next/swc-linux-x64-gnu": "15.5.4",
"@next/swc-linux-x64-musl": "15.5.4",
"@next/swc-win32-arm64-msvc": "15.5.4",
"@next/swc-win32-x64-msvc": "15.5.4",
"@next/swc-darwin-arm64": "15.5.7",
"@next/swc-darwin-x64": "15.5.7",
"@next/swc-linux-arm64-gnu": "15.5.7",
"@next/swc-linux-arm64-musl": "15.5.7",
"@next/swc-linux-x64-gnu": "15.5.7",
"@next/swc-linux-x64-musl": "15.5.7",
"@next/swc-win32-arm64-msvc": "15.5.7",
"@next/swc-win32-x64-msvc": "15.5.7",
"sharp": "^0.34.3"
},
"peerDependencies": {

View File

@@ -18,7 +18,7 @@
"clsx": "^2.1.1",
"i18next": "^23.15.0",
"lucide-react": "^0.460.0",
"next": "^15.1.3",
"next": "15.5.7",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",

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

@@ -20,6 +20,38 @@ interface StakeFormProps {
defaultMode?: Mode;
}
// Helper component for slippage warnings
function SlippageWarning({
slippage,
action,
isError
}: {
slippage: number;
action: string;
isError: boolean;
}) {
if (isError) {
return (
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium"> Slippage Exceeds 5%</p>
<p className="text-xs text-destructive/80 mt-1">
The estimated slippage for this {action} is {Math.abs(slippage).toFixed(2)}%.
We cannot process this {action} as you may lose too much money due to the high slippage.
</p>
</div>
);
}
return (
<div className="px-4 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<p className="text-sm text-yellow-600 dark:text-yellow-500 font-medium"> High Slippage Warning</p>
<p className="text-xs text-yellow-600/80 dark:text-yellow-500/80 mt-1">
The estimated slippage for this {action} is {Math.abs(slippage).toFixed(2)}%. You may lose money due to low liquidity in this pool.
</p>
</div>
);
}
interface TokenInfo {
address: `0x${string}`;
symbol: string;
@@ -141,23 +173,48 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
}, [stakeAmount, selectedToken, mode]);
// Fetch swap mint amounts (for stake mode)
const { swapMintAmounts, loading: swapMintLoading } = useSwapMintAmounts(
const { swapMintAmounts, loading: swapMintLoading, error: swapMintError } = useSwapMintAmounts(
mode === 'stake' ? selectedPool?.address : undefined,
mode === 'stake' ? inputTokenIndex : undefined,
mode === 'stake' ? maxAmountIn : undefined
mode === 'stake' ? maxAmountIn : undefined,
mode === 'stake' && selectedToken ? selectedToken.decimals : undefined
);
// Fetch burn swap amounts (for unstake mode, only when not redeeming all)
const { burnSwapAmounts, loading: burnSwapLoading } = useBurnSwapAmounts(
const { burnSwapAmounts, loading: burnSwapLoading, error: burnSwapError } = useBurnSwapAmounts(
mode === 'unstake' && !redeemAll ? selectedPool?.address : undefined,
mode === 'unstake' && !redeemAll ? maxAmountIn : undefined,
mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined,
undefined, // lpTokenPrice - not needed for burnSwap custom calculation
mode === 'unstake' && !redeemAll && selectedToken ? selectedToken.decimals : undefined
);
// Check if calculated slippage exceeds 5%
const slippageExceedsLimit = useMemo(() => {
if (mode === 'stake' && swapMintAmounts?.calculatedSlippage !== undefined) {
return Math.abs(swapMintAmounts.calculatedSlippage) > 5;
}
return false;
}, [mode, swapMintAmounts]);
console.log('burn swap maounts', burnSwapAmounts)
// Check if slippage is high (> 2%)
const slippageIsHigh = useMemo(() => {
if (mode === 'stake' && swapMintAmounts?.calculatedSlippage !== undefined) {
return Math.abs(swapMintAmounts.calculatedSlippage) > 2;
}
if (mode === 'unstake' && !redeemAll && burnSwapAmounts?.calculatedSlippage !== undefined) {
return Math.abs(burnSwapAmounts.calculatedSlippage) > 2;
}
return false;
}, [mode, swapMintAmounts, burnSwapAmounts, redeemAll]);
// Check if unstake slippage exceeds 5%
const unstakeSlippageExceedsLimit = useMemo(() => {
if (mode === 'unstake' && !redeemAll && burnSwapAmounts?.calculatedSlippage !== undefined) {
return Math.abs(burnSwapAmounts.calculatedSlippage) > 5;
}
return false;
}, [mode, burnSwapAmounts, redeemAll]);
// Fetch burn amounts (for unstake mode when redeeming all)
const { burnAmounts, loading: burnAmountsLoading } = useBurnAmounts(
@@ -669,6 +726,58 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
</div>
)}
{/* Error messages for output zero */}
{mode === 'stake' && swapMintError && stakeAmount && (
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium"> Cannot Process Stake</p>
<p className="text-xs text-destructive/80 mt-1">
{swapMintError}
</p>
</div>
)}
{mode === 'unstake' && !redeemAll && burnSwapError && stakeAmount && (
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium"> Cannot Process Unstake</p>
<p className="text-xs text-destructive/80 mt-1">
{burnSwapError}
</p>
</div>
)}
{/* Slippage warnings - consolidated for both stake and unstake modes */}
{mode === 'stake' && !swapMintError && slippageExceedsLimit && swapMintAmounts?.calculatedSlippage !== undefined && (
<SlippageWarning
slippage={swapMintAmounts.calculatedSlippage}
action="stake"
isError={true}
/>
)}
{mode === 'stake' && !swapMintError && !slippageExceedsLimit && slippageIsHigh && swapMintAmounts?.calculatedSlippage !== undefined && (
<SlippageWarning
slippage={swapMintAmounts.calculatedSlippage}
action="stake"
isError={false}
/>
)}
{mode === 'unstake' && !redeemAll && !burnSwapError && unstakeSlippageExceedsLimit && burnSwapAmounts?.calculatedSlippage !== undefined && (
<SlippageWarning
slippage={burnSwapAmounts.calculatedSlippage}
action="unstake"
isError={true}
/>
)}
{mode === 'unstake' && !redeemAll && !burnSwapError && !unstakeSlippageExceedsLimit && slippageIsHigh && burnSwapAmounts?.calculatedSlippage !== undefined && (
<SlippageWarning
slippage={burnSwapAmounts.calculatedSlippage}
action="unstake"
isError={false}
/>
)}
{/* Burn Swap Amounts Display (Unstake Mode) */}
{mode === 'unstake' && !redeemAll && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && (
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
@@ -717,10 +826,10 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
!selectedPool ||
isAmountExceedingBalance ||
(mode === 'stake'
? (!selectedToken || isSwapMinting)
? (!selectedToken || isSwapMinting || slippageExceedsLimit || !!swapMintError)
: (redeemAll
? isBurning
: (!selectedToken || inputTokenIndex === undefined || isBurnSwapping)))
: (!selectedToken || inputTokenIndex === undefined || isBurnSwapping || unstakeSlippageExceedsLimit || !!burnSwapError)))
}
>
{!isConnected

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);
@@ -94,6 +96,8 @@ export function SwapForm() {
const swapResult = swapAmounts[0]; // Get the first (and should be only) result
const formattedAmount = formatUnits(swapResult.amountOut, selectedToToken.decimals);
setToAmount(formattedAmount);
} else {
setToAmount('');
}
}, [swapAmounts, selectedToToken, hasInsufficientBalance]);
@@ -395,13 +399,26 @@ export function SwapForm() {
Math.abs(swapAmounts[0].calculatedSlippage) > currentSlippage
) && (
<div className="px-4 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<p className="text-sm text-yellow-600 dark:text-yellow-500 font-medium"> High Slippage Warning</p>
<p className="text-sm text-yellow-600 dark:text-yellow-500 font-medium"> Slippage Exceeds Your Tolerance</p>
<p className="text-xs text-yellow-600/80 dark:text-yellow-500/80 mt-1">
The estimated slippage for this swap is {Math.abs(swapAmounts[0].calculatedSlippage).toFixed(2)}%. You may lose money due to low liquidity in this pool.
The estimated slippage for this swap is {Math.abs(swapAmounts[0].calculatedSlippage).toFixed(2)}%, which exceeds your maximum slippage setting of {currentSlippage}%. This swap may result in less favorable pricing than expected due to low liquidity.
</p>
</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">
@@ -491,7 +508,7 @@ export function SwapForm() {
</span>
</div>
<p className="text-xs text-muted-foreground mt-2">
Your transaction will revert if the price changes unfavorably by more than this percentage.
You will be warned if the slippage exceeds this setting, and you can choose whether to proceed with the trade.
</p>
</div>
</div>

View File

@@ -4,18 +4,16 @@ export default function TosCard() {
return (
<div className="max-w-5xl mx-auto p-5">
<div className="bg-card rounded-lg shadow-md p-6 border">
<h1 className="text-2xl font-semibold text-center mb-4">Dexorder Terms of Service</h1>
<h1 className="text-2xl font-semibold text-center mb-4">Terms of Service</h1>
{/* MAKE SURE TO UPDATE THE VERSION VARIABLE AS WELL */}
<p className="text-center mb-4">Last Updated November 18, 2024</p>
<div className="mb-4 leading-relaxed">
Please read these Terms of Service (the "<b>Terms</b>") and our{' '}
<a href="https://dexorder.com/privacy-policy" target="dexorder">
Privacy Policy
</a>{' '}
Please read these Terms of Service (the "<b>Terms</b>")
carefully because they govern your use of the website (and all subdomains and subpages
thereon) located at dexorder.com, including without limitation the subdomains
app.dexorder.com and www.dexorder.com (collectively, the "<b>Site</b>"), and the Dexorder
thereon) located at liquidity.party, including without limitation the subdomains
app.liquidity.party and www.liquidity.party (collectively, the "<b>Site</b>"), and the
Liquidity Party
web application graphical user interface and any other services accessible via the Site
(together with the Site, web application, and other services, collectively, the "
<b>Dexorder Service</b>") offered by Dexorder Trading Services Ltd. ("<b>Dexorder</b>," "
@@ -45,29 +43,14 @@ export default function TosCard() {
<div className="mb-4 leading-relaxed">
(a) The Dexorder Service allows you to access an online web application graphical user
interface (the "<b>App</b>") which enables you to interact with a protocol consisting of a
set of smart contracts (the "<b>Smart Contracts</b>") and to create and interact with a
user controlled smart contract involving digital assets over which only you have upgrade
authority (a "<b>Vault</b>" and together with the Smart Contracts, the "<b>Protocol</b>").
You may use your Vault to send signals to, interact with, and initiate actions on third
party smart contract blockchain protocols ("<b>Interactions</b>") operating on
decentralized exchanges (e.g., Uniswap) ("<b>DEXs</b>"). Certain Interactions may require
threshold parameters to be met and that a third party transmit an oracle related activation
signal (the "<b>Activation Signal</b>") to the Vault in order to effectuate your commands
(such party being an "<b>Oracle</b>"). Dexorder may in the ordinary course of events be an
Oracle ("<b>Dexorder Oracle</b>"), but bears no obligation nor promise to do so on an
ongoing basis, and you may send such Activation Signal yourself or utilize a third-party
Oracle to do so. Further, Dexorder is not and does not offer a digital wallet, and has no
custody or control over your digital wallet, which is never accessible by Dexorder, and only
users, and not Dexorder, may provide or withdraw tokens to be held by Vault. From time to
time, the Services may include making recommendations with respect to technical changes that
only you may accept and implement.
set of smart contracts (the "<b>Protocol</b>").
You may use the <b>App</b> to send signals to, interact with, and initiate actions on the
decentralized exchange ("<b>DEX</b>").
</div>
<div className="mb-4 leading-relaxed">
(b) <b>Interface</b>. The Dexorder Service provides you with access to the Protocol, which
is a user controlled, non-custodial protocol, upgradeable only by your actions and consent,
deployed on the blockchains indicated on our Site, and provides information and interaction
capabilities with other blockchain related service providers. All information provided in
(b) <b>Interface</b>. The Dexorder Service provides you with access to the Protocol.
All information provided in
connection with your access and use of the Dexorder Service is for informational purposes
only. You should not take, or refrain from taking, any action based on any information
contained on the Dexorder Service or any other information that we make available at any
@@ -208,12 +191,8 @@ export default function TosCard() {
<div className="mb-4 leading-relaxed ml-8">
(i) Subject to your compliance with these Terms, Dexorder will use its commercially
reasonable efforts to provide you with access to the Dexorder Service and to cause your
Interactions to be executed on the applicable DEX in accordance with Dexorder's Execution
Policy located at{' '}
<a href="https://dexorder.com/execution-policy">
https://dexorder.com/execution-policy
</a>{' '}
("<b>Execution Policy</b>"), however from time to time the Site and the Dexorder Service may
Interactions to be executed on the Protocol, however from time to time the Site and
the Dexorder Service may
be inaccessible or inoperable for any reason, including, without limitation: (a) if an
Interaction repeatedly fails to be executed (such as due to an error in Interaction
execution or a malfunction in the Dexorder Service); (b) equipment malfunctions; (c)
@@ -221,8 +200,7 @@ export default function TosCard() {
contractors may undertake from time to time; (d) causes beyond Dexorder's control or that
Dexorder could not reasonably foresee; (e) disruptions and temporary or permanent
unavailability of underlying blockchain infrastructure; (f) unavailability of third-party
service providers or external partners for any reason; or (g) an Activation Signal not being
sent.
service providers or external partners for any reason.
</div>
<div className="mb-4 leading-relaxed ml-8">
(ii) the Site and the Dexorder Service may evolve, which means Dexorder may apply changes,
@@ -245,11 +223,9 @@ export default function TosCard() {
<h2 className="text-xl font-semibold mt-6 mb-4">6. Interactions; Fees</h2>
<div className="mb-4 leading-relaxed">
(a) <u>Interactions</u>. In order to effectuate Interactions, you may need to transfer
digital assets (e.g., tokens) to the Vault. You acknowledge that you may use the Dexorder
Services to process and cause Interactions to be operate with an applicable DEX, including
without limitation the transfer of digital assets via the DEX in accordance with the
Interaction. For clarity, the Vault is a smart contract automatically controlled by the
blockchain. Dexorder is an interface to that smart contract, and does not offer a digital
digital assets (e.g., tokens). You acknowledge that you may use the Dexorder
Services to process and cause Interactions to operate on the Protocol.
Dexorder is an interface to that smart contract, and does not offer a digital
wallet and has no custody or control over your digital wallet or any digital assets or
cryptocurrency, which is never accessible by Dexorder.
</div>
@@ -257,8 +233,8 @@ export default function TosCard() {
(b) <u>Fees</u>.
</div>
<div className="mb-4 leading-relaxed ml-8">
(i) Dexorder charges fees upfront for usage of the Dexorder Services at the time of user
Interactions ("<b>Fees</b>"). You agree to pay all applicable Fees upfront to Dexorder, in
(i) Dexorder charges fees for usage of the Dexorder Services at the time of user
Interactions ("<b>Fees</b>"). You agree to pay all applicable Fees to Dexorder, in
the amounts communicated or presented to you via the Dexorder Service in connection with
usage of the Dexorder Service. Each party shall be responsible for all Taxes imposed on its
income or property.
@@ -416,8 +392,7 @@ export default function TosCard() {
<h2 className="text-xl font-semibold mt-6 mb-4">9. Links to Third Party Websites or Resources</h2>
<div className="mb-4 leading-relaxed">
The Dexorder Service may allow you to access third-party websites, integrations, or other
resources, including the DEX, services providing Activation Signals, and any bridge between
the DEX and any third party protocols (collectively, "<b>Third Party Resources</b>"). We
resources, including the Protocol (collectively, "<b>Third Party Resources</b>"). We
provide access only as a convenience and are not responsible for the content, products or
services on or available from those resources or links displayed on such websites. You
acknowledge sole responsibility for and assume all risk arising from, your use of any
@@ -440,7 +415,7 @@ export default function TosCard() {
<div className="mb-4 leading-relaxed">
THE DEXORDER SERVICE (INCLUDING WITHOUT LIMITATION THE VAULT) AND ANY CONTENT CONTAINED
THEREIN, AS WELL AS THE PROTOCOL, THE DEXORDER ORACLE, AND ANY ASSOCIATED PROTOCOL OR
BLOCKCHAIN MESSAGING FUNCTIONALITY SUCH AS ACTIVATION SIGNALS UNDERLYING THE DEXORDER
BLOCKCHAIN MESSAGING FUNCTIONALITY UNDERLYING THE DEXORDER
SERVICE (TOGETHER, THE "<b>UTILITIES</b>"), ARE PROVIDED "AS IS," WITHOUT WARRANTY OF ANY
KIND. WITHOUT LIMITING THE FOREGOING, WE EXPLICITLY DISCLAIM ANY IMPLIED WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT AND NON-INFRINGEMENT, AND
@@ -476,25 +451,6 @@ export default function TosCard() {
USE OF VIRUSES, PHISHING, BRUTEFORCING OR OTHER MEANS OF ATTACK AGAINST ANY BLOCKCHAIN
NETWORK UNDERLYING THE UTILITIES.
</div>
<div className="mb-4 leading-relaxed">
THE UTILITIES MAY INCLUDE THE PLACEMENT OR EXECUTION OF AN ACTIVATION SIGNAL TO A USER VAULT
WITH RESPECT TO USER DEFINED INTERACTIONS (E.G., VIA TRANSMITTING AN ACTIVATION SIGNAL WHICH
TRIGGERS INTERACTION EXECUTION), HOWEVER, ANYONE, INCLUDING YOU OR ANY THIRD PARTY, MAY
CAUSE AN INTERACTION TO BE EXECUTED (SUCH AS BY TRANSMITTING THE APPLICABLE ACTIVATION
SIGNAL), AND NOTWITHSTANDING ANYTHING TO THE CONTRARY IN THESE TERMS, DEXORDER DOES NOT
GUARANTEE THE PLACEMENT OR EXECUTION OR ANY INTERACTION, INCLUDING WITHOUT LIMITATION THAT
DEXORDER WILL TRANSMIT ANY PARTICULAR ACTIVATION SIGNAL TO TRIGGER EXECUTION OF ANY
INTERACTION, OR THAT AN INTERACTION WILL OTHERWISE BE PROPERLY CARRIED OUT PURSUANT TO A
VAULT. YOU ACKNOWLEDGE AND AGREE THAT YOU WILL NOT RELY ON THE UTILITIES TO CARRY OUT
PLACEMENTS OR EXECUTIONS OF INTERACTIONS. IF ANY PARTICULAR INTERACTION THAT IS PLACED IS
NOT EXECUTED BY DEXORDER IN A TIMELY MANNER, YOU MAY CAUSE SUCH INTERACTION TO BE EXECUTED
YOURSELF OR BY ENGAGING A THIRD PARTY SERVICE PROVIDER TO DO SO (E.G., BY TRANSMITTING THE
APPLICABLE ACTIVATION SIGNAL YOURSELF OR ENGAGING A THIRD PARTY TO DO SO). YOU ACCEPT THE
INHERENT RISK THAT ANY PARTICULAR INTERACTION MAY NOT BE EXECUTED, INCLUDING WITHOUT
LIMITATION DUE TO BAD ACTORS OR THE MALFUNCTION OF THE UTILITIES, AND DEXORDER HEREBY
DISCLAIMS ANY AND ALL LIABILITY AND RESPONSIBILITY IN CONNECTION WITH THE PLACEMENT OR
EXECUTION OF INTERACTIONS AND THE VAULT.
</div>
<div className="mb-4 leading-relaxed">
THE UTILITIES MAY NOT BE AVAILABLE DUE TO ANY NUMBER OF FACTORS INCLUDING, BUT NOT LIMITED
TO, PERIODIC SYSTEM MAINTENANCE, SCHEDULED OR UNSCHEDULED, ACTS OF GOD, UNAUTHORIZED ACCESS,

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

@@ -183,8 +183,8 @@ const IPartyInfoABI = [
"outputs": [
{
"name": "",
"type": "int128",
"internalType": "int128"
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"

View File

@@ -415,6 +415,19 @@ const IPartyPoolABI = [
],
"stateMutability": "payable"
},
{
"type": "function",
"name": "mintImpl",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "name",
@@ -605,6 +618,19 @@ const IPartyPoolABI = [
],
"stateMutability": "view"
},
{
"type": "function",
"name": "swapImpl",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "swapMint",

View File

@@ -8,7 +8,6 @@ import IPartyPoolABI from '@/contracts/IPartyPoolABI';
import IPartyPoolViewerABI from '@/contracts/IPartyPoolViewerABI';
import IPartyInfoABI from '@/contracts/IPartyInfoABI';
import { ERC20ABI } from '@/contracts/ERC20ABI';
import { calculateSlippage } from './usePartyPool';
// Helper function to format large numbers with K, M, B suffixes
function formatTVL(value: number): string {
@@ -100,6 +99,8 @@ export interface SwapRoute {
poolAddress: `0x${string}`;
inputTokenIndex: number;
outputTokenIndex: number;
inputTokenDecimal: number;
outputTokenDecimal: number;
}
export interface AvailableToken {
@@ -197,68 +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);
// Skip tokens with the same symbol as the selected token
if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) {
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,
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,
});
}
}
@@ -305,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);
@@ -465,42 +528,12 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
let tvlStr: string | undefined;
if (isWorking) {
// Fetch pool price (use first token as quote, index 0)
console.log('fetching pool price')
try {
const priceRaw = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'poolPrice',
args: [poolAddress, BigInt(0)],
});
const price = BigInt(priceRaw as bigint | number);
if (price === 0n) {
priceStr = undefined;
} else {
// Convert Q64 format to decimal (price = priceValue / 2^64)
const Q64 = 2n ** 64n;
const isNegative = price < 0n;
const absPrice = isNegative ? -price : price;
const priceFloat = Number(absPrice) / Number(Q64);
const finalPrice = isNegative ? -priceFloat : priceFloat;
priceStr = `$${finalPrice.toFixed(4)}`;
}
} catch (err) {
console.error(`Error fetching pool price for ${poolAddress}:`, err);
priceStr = undefined;
}
// Calculate TVL (approximate by getting first token balance and doubling it)
// Fetch token decimals and balance first (needed for both price and TVL)
try {
if (tokens && tokens.length > 0) {
const firstTokenAddress = tokens[0];
// Get token decimals and balance
const [decimals, balance] = await Promise.all([
// Get token decimals, balance, and pool price in parallel
const [decimals, balance, priceRaw] = await Promise.all([
publicClient.readContract({
address: firstTokenAddress as `0x${string}`,
abi: ERC20ABI,
@@ -512,16 +545,36 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
functionName: 'balanceOf',
args: [poolAddress],
}) as Promise<bigint>,
publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'poolPrice',
args: [poolAddress, BigInt(0)],
}),
]);
// Convert balance to float and double it for total TVL approximation
const tokenBalance = Number(balance) / Math.pow(10, decimals);
const approximateTVL = tokenBalance * 3;
// Calculate pool price using actual token decimals
const price = BigInt(priceRaw as bigint | number);
if (price === 0n) {
priceStr = undefined;
} else {
// Convert Q64 format to decimal (price = priceValue / 2^64)
const Q64 = 2n ** 64n;
const priceFloat = Number(price) / Number(Q64);
// Adjust for token decimals (poolPrice assumes 18 decimals, adjust based on actual token decimals)
const finalPrice = priceFloat * (Math.pow(10, 18) / Math.pow(10, decimals));
priceStr = `$${finalPrice.toFixed(4)}`;
}
// Calculate TVL (approximate by getting first token balance and multiplying by number of tokens)
const tokenBalance = Number(balance) / Math.pow(10, decimals);
const approximateTVL = tokenBalance * tokens.length;
tvlStr = formatTVL(approximateTVL);
}
} catch (err) {
console.error(`Error fetching TVL for ${poolAddress}:`, err);
console.error(`Error fetching pool price/TVL for ${poolAddress}:`, err);
priceStr = undefined;
tvlStr = undefined;
}
}
@@ -580,8 +633,7 @@ export function useSwapMintAmounts(
poolAddress: `0x${string}` | undefined,
inputTokenIndex: number | undefined,
maxAmountIn: bigint | undefined,
lpTokenPrice?: number, // Market price of the LP token in decimal format
inputTokenDecimals?: number // Decimals of the input token
inputTokenDecimals: number | undefined // Decimals of the input token
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
@@ -602,8 +654,11 @@ export function useSwapMintAmounts(
}
const fetchSwapMintAmounts = async () => {
if (!publicClient) {
setLoading(false);
if (!publicClient) return;
// Early validation
if (inputTokenDecimals === undefined) {
setError('inputTokenDecimals is required but was undefined');
return;
}
@@ -611,9 +666,7 @@ export function useSwapMintAmounts(
setLoading(true);
setError(null);
// Get chain ID and contract address
const chainId = await publicClient.getChainId();
// @ts-ignore
const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!partyInfoAddress) {
@@ -622,7 +675,6 @@ export function useSwapMintAmounts(
return;
}
// Call swapMintAmounts function
const result = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyPoolViewerABI,
@@ -630,45 +682,30 @@ export function useSwapMintAmounts(
args: [poolAddress, BigInt(inputTokenIndex), maxAmountIn],
}) as readonly [bigint, bigint, bigint];
// Fetch the market price if not provided
let marketPrice = lpTokenPrice;
if (marketPrice === undefined) {
try {
console.log('input token index', inputTokenIndex)
// Get the pool price (price of the pool in terms of the input token)
const poolPriceInt128 = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'poolPrice',
args: [poolAddress, BigInt(inputTokenIndex)],
}) as bigint;
// Convert Q64 format to decimal (price = priceValue / 2^64)
// poolPrice returns how much 1 LP token is worth in terms of the input token
marketPrice = Number(poolPriceInt128) / 2 ** 64;
console.log('swapMintAmounts fetched marketPrice (poolPrice)', marketPrice);
} catch (priceErr) {
console.error('Error fetching poolPrice:', priceErr);
}
}
// Calculate slippage if market price is available
// Fetch and calculate pool price
let poolPrice: number | undefined;
let calculatedSlippage: number | undefined;
if (marketPrice !== undefined) {
try {
// For swapMint:
// - marketPrice: how much 1 LP token is worth in input tokens (from poolPrice)
// - swapOutputAmount: LP tokens minted (result[2])
// - swapInputAmount: input token amount used (maxAmountIn)
// - inFee: fee charged (result[1])
console.log('LP minted', result[1])
// Convert result[1] to token price using poolPrice: result[1] * poolPrice
const lpMintedInTokenPrice = BigInt(Math.floor(Number(result[1]) * marketPrice));
calculatedSlippage = calculateSlippage(marketPrice, lpMintedInTokenPrice, result[0], result[2]);
console.log('🎯 swapMint calculatedSlippage: ' + calculatedSlippage.toFixed(4) + '%');
} catch (slippageErr) {
console.error(`Error calculating slippage for swapMint:`, slippageErr);
}
try {
const poolPriceInt128 = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'poolPrice',
args: [poolAddress, BigInt(inputTokenIndex)],
}) as bigint;
const basePrice = Number(poolPriceInt128) / (2 ** 64);
poolPrice = 1 / (basePrice * Math.pow(10, 18 - inputTokenDecimals));
// Calculate slippage
const decimalsMultiplier = Math.pow(10, inputTokenDecimals);
const lpMinted = Number(result[1]) / Math.pow(10, 18);
const amountIn = Number(result[0]) / decimalsMultiplier;
const fee = Number(result[2]) / decimalsMultiplier;
const swapPrice = lpMinted / (amountIn - fee);
calculatedSlippage = ((poolPrice - swapPrice) / poolPrice) * 100;
} catch (priceErr) {
console.error('Error fetching poolPrice or calculating slippage:', priceErr);
}
setSwapMintAmounts({
@@ -679,7 +716,8 @@ export function useSwapMintAmounts(
});
} catch (err) {
console.error('Error calling swapMintAmounts:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch swap mint amounts');
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch swap mint amounts';
setError(errorMessage);
setSwapMintAmounts(null);
} finally {
setLoading(false);
@@ -687,7 +725,7 @@ export function useSwapMintAmounts(
};
fetchSwapMintAmounts();
}, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn, lpTokenPrice]);
}, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn, inputTokenDecimals]);
return {
swapMintAmounts,
@@ -701,8 +739,7 @@ export function useBurnSwapAmounts(
poolAddress: `0x${string}` | undefined,
lpAmount: bigint | undefined,
inputTokenIndex: number | undefined,
lpTokenPrice?: number, // Market price of the LP token in decimal format
tokenDecimals?: number // Decimals of the output token
tokenDecimals: number | undefined // Decimals of the output token
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
@@ -731,31 +768,17 @@ export function useBurnSwapAmounts(
try {
setLoading(true);
setError(null);
console.log('fetching swap amounts')
// Get chain ID and contract address
const chainId = await publicClient.getChainId();
// @ts-ignore
const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!partyInfoAddress) {
console.log('errores here')
setError('PartyInfo contract not found for current chain');
setBurnSwapAmounts(null);
return;
}
console.log('fetching swap amounts 2')
// Log inputs
console.log('🔍 burnSwapAmounts INPUTS:', {
chainId: chainId.toString(),
rpcUrl: publicClient.transport?.url || 'Unknown',
poolAddress,
lpAmount: lpAmount.toString(),
inputTokenIndex,
partyInfoAddress,
});
// Call burnSwapAmounts function - returns [amountOut, outFee]
const result = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
@@ -764,12 +787,6 @@ export function useBurnSwapAmounts(
args: [poolAddress, lpAmount, BigInt(inputTokenIndex)],
}) as readonly [bigint, bigint];
// Log raw result
console.log('📊 burnSwapAmounts RAW RESULT:', {
resultArray: result,
amountOut: result[0].toString(),
outFee: result[1].toString(),
});
// Calculate slippage for burnSwap using poolPrice
let calculatedSlippage: number | undefined;
if (tokenDecimals !== undefined) {
@@ -783,7 +800,8 @@ export function useBurnSwapAmounts(
}) as bigint;
// Convert Q64 format to decimal (price = priceValue / 2^64)
const marketPrice = Number(poolPriceInt128) / 2 ** 64;
let poolPrice = Number(poolPriceInt128) / 2 ** 64;
poolPrice = (poolPrice * Math.pow(10, 18 - tokenDecimals));
// For burnSwap: swapPrice = (outAmount + fee) / lpAmount
// Need to apply decimal corrections: outAmount and fee are in token decimals, lpAmount is in 18 decimals
@@ -792,21 +810,8 @@ export function useBurnSwapAmounts(
const lpAmountDecimal = Number(lpAmount) / Math.pow(10, 18); // LP tokens have 18 decimals
const swapPrice = (outAmountDecimal + feeDecimal) / lpAmountDecimal;
calculatedSlippage = ((poolPrice - swapPrice) / poolPrice) * 100;
// Calculate slippage percentage: ((swapPrice - marketPrice) / marketPrice) * 100
calculatedSlippage = ((swapPrice - marketPrice) / marketPrice) * 100;
console.log('burnSwap slippage calculation:', {
marketPrice,
swapPrice,
calculatedSlippage,
outAmountDecimal,
feeDecimal,
lpAmountDecimal,
outAmount: result[0].toString(),
fee: result[1].toString(),
lpAmount: lpAmount.toString(),
});
} catch (slippageErr) {
console.error(`Error calculating slippage for burnSwap:`, slippageErr);
}
@@ -818,17 +823,11 @@ export function useBurnSwapAmounts(
calculatedSlippage,
};
// Log parsed result
console.log('✅ burnSwapAmounts PARSED:', {
amountOut: parsedAmounts.amountOut.toString(),
outFee: parsedAmounts.outFee.toString(),
calculatedSlippage: parsedAmounts.calculatedSlippage,
});
setBurnSwapAmounts(parsedAmounts);
} catch (err) {
console.error('Error calling burnSwapAmounts:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch burn swap amounts');
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch burn swap amounts';
setError(errorMessage);
setBurnSwapAmounts(null);
} finally {
setLoading(false);
@@ -836,7 +835,7 @@ export function useBurnSwapAmounts(
};
fetchBurnSwapAmounts();
}, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex, lpTokenPrice, tokenDecimals]);
}, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex, tokenDecimals]);
return {
burnSwapAmounts,

View File

@@ -21,13 +21,12 @@ const Q96 = 1n << 96n;
*/
export function calculateSlippage(
marketPrice: number,
swapOutputAmount: bigint,
swapInputAmount: bigint,
swapFee: bigint
swapOutputAmount: number,
swapInputAmount: number,
swapFee: number
): number {
// Calculate actual swap price with decimal correction
const swapPrice = Number(swapOutputAmount) / (Number(swapInputAmount) - Number(swapFee));
const swapPrice = swapOutputAmount / ((swapInputAmount) - (swapFee));
// Calculate slippage percentage: ((swapPrice - marketPrice) / marketPrice) * 100
const slippage = ((marketPrice - swapPrice) / marketPrice) * 100;
@@ -121,18 +120,6 @@ export function useSwapAmounts(
setLoading(true);
const amountInTokenUnits = parseUnits(fromAmount, fromTokenDecimals);
// Calculate limit price based on slippage tolerance
// limitPrice in Q96 format = Q96 * (100 + slippage%) / 100
// This represents the maximum acceptable price ratio (1 + slippage%)
const slippageBasisPoints = BigInt(Math.floor(slippagePercent * 100)); // Convert to basis points (0.5% = 50)
const limitPrice = (Q96 * (10000n + slippageBasisPoints)) / 10000n;
console.log('Limit Price Calculation:', {
slippagePercent,
slippageBasisPoints: slippageBasisPoints.toString(),
limitPriceQ96: limitPrice.toString(),
Q96: Q96.toString(),
});
const results: SwapAmountResult[] = [];
const chainId = await publicClient.getChainId();
@@ -147,8 +134,8 @@ export function useSwapAmounts(
// Evaluate ALL routes for this token
for (const route of token.swapRoutes) {
try {
// Get swap amounts
const swapResult = await publicClient.readContract({
// Get swap amounts with NO LIMIT
const [swapInputAmount, swapOutputAmount, inFee] = await publicClient.readContract({
address: route.poolAddress,
abi: IPartyPoolABI,
functionName: 'swapAmounts',
@@ -156,12 +143,10 @@ export function useSwapAmounts(
BigInt(route.inputTokenIndex),
BigInt(route.outputTokenIndex),
amountInTokenUnits,
limitPrice,
0n, // NO LIMIT
],
}) as readonly [bigint, bigint, bigint];
const [amountIn, amountOut, fee] = swapResult;
// Get kappa for this pool
const kappa = await publicClient.readContract({
address: route.poolAddress,
@@ -173,19 +158,6 @@ export function useSwapAmounts(
let calculatedSlippage: number | undefined;
if (partyInfoAddress) {
try {
// Get swap amounts with NO LIMIT (0 means no price limit)
const [swapInputAmount, swapOutputAmount, inFee] = await publicClient.readContract({
address: route.poolAddress,
abi: IPartyPoolABI,
functionName: 'swapAmounts',
args: [
BigInt(route.inputTokenIndex),
BigInt(route.outputTokenIndex),
amountInTokenUnits,
0n, // NO LIMIT
],
}) as readonly [bigint, bigint, bigint];
// Get the current market price from PoolInfo
const priceInt128 = await publicClient.readContract({
address: partyInfoAddress,
@@ -194,11 +166,17 @@ export function useSwapAmounts(
args: [route.poolAddress, BigInt(route.inputTokenIndex), BigInt(route.outputTokenIndex)],
}) as bigint;
// Convert Q64 format to decimal (price = priceValue / 2^64)
const marketPrice = Number(priceInt128) / 2 ** 64;
// Convert Q128 format to decimal (price = priceValue / 2^128)
// Then apply decimal conversion: 10^18 / 10^outputTokenDecimals
const priceQ128 = Number(priceInt128) / 2 ** 128;
const decimalAdjustment = 10 ** (route.outputTokenDecimal - route.inputTokenDecimal);
const marketPrice = priceQ128 / decimalAdjustment;
const swapOutputAmountDecimal = Number(swapOutputAmount) / 10 ** route.outputTokenDecimal;
const swapInputAmountDecimal = Number(swapInputAmount) / 10 ** route.inputTokenDecimal;
const swapFeeDecimal = Number(inFee) / 10 ** route.inputTokenDecimal;
// Calculate slippage using the reusable function
calculatedSlippage = calculateSlippage(marketPrice, swapOutputAmount, swapInputAmount, inFee);
calculatedSlippage = calculateSlippage(marketPrice, swapOutputAmountDecimal, swapInputAmountDecimal, swapFeeDecimal);
console.log('calculatedSlippage', calculatedSlippage)
} catch (slippageErr) {
console.error(`Error calculating slippage for ${token.symbol}:`, slippageErr);
@@ -208,9 +186,9 @@ export function useSwapAmounts(
routeResults.push({
tokenAddress: token.address,
tokenSymbol: token.symbol,
amountIn,
amountOut,
fee,
amountIn: swapInputAmount,
amountOut: swapOutputAmount,
fee: inFee,
poolAddress: route.poolAddress,
kappa,
inputTokenIndex: route.inputTokenIndex,
@@ -382,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:', {
@@ -394,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,
@@ -411,7 +387,7 @@ export function useSwap() {
BigInt(inputTokenIndex),
BigInt(outputTokenIndex),
maxAmountIn,
limitPrice,
0n, // no limit price
deadline,
false, // unwrap
'0x', // cbData (empty bytes)