Compare commits

...

24 Commits

Author SHA1 Message Date
d207ef6dca fixing the TVL calculation 2025-12-19 10:40:16 -04:00
0e5921255e speeding up getAllTokens by using multicall instead of sequentially calling details for each token 2025-12-19 10:35:59 -04:00
1ac26aeec0 using viem for the uniswap quote and hiding the component since its not ready 2025-12-10 17:05:39 -04:00
a2a036818d getting quotes from unsiwap 2025-12-10 17:05:39 -04:00
tim
1c2e267136 removed .env-secret; fixed .gitignore; updated package.json for deploy pool script 2025-12-10 12:30:13 -04:00
tim
b2abf2073e updated etherscan link 2025-12-08 23:54:57 -04:00
9835d67e54 removing limitPrice calucating from the swap execution 2025-12-08 18:17:49 -04:00
b0b050f4be removing limit price from swap. updating slippage warnings and updating ABIs 2025-12-08 17:52:08 -04:00
f43db3391b removing abs in decimal adjustment calculation for slippage 2025-12-08 17:52:08 -04:00
334e9f4f53 upgrading next.js related to this patch: https://nextjs.org/blog/CVE-2025-66478 2025-12-08 17:52:08 -04:00
tim
9d2ee39d1a ToS update 2025-12-04 20:59:33 -04:00
d4e41821a6 adding reactivity and error message handling 2025-12-03 17:43:17 -04:00
d07ff55c13 slippage for swaps 2025-12-03 16:53:57 -04:00
4523195b78 price bug fix 2025-12-03 13:33:42 -04:00
068286919a slippage for swapMint and burnSwap 2025-12-02 19:03:56 -04:00
7b8114267e making the buy input box in the swap-form ready only 2025-12-01 13:04:05 -04:00
f3277b45ab diabling swab button if slippage exceed 5% 2025-12-01 13:00:02 -04:00
fe02d94d11 WIP: slipage for swapAmounts 2025-12-01 12:40:34 -04:00
ff9a718522 fixing fee/lp amount display bug 2025-12-01 12:40:34 -04:00
e36f6011c9 create pools script (uncommenting approvals and adding reset USDT) 2025-12-01 12:40:34 -04:00
99929d8db8 [wif] slippage for burnSwap 2025-12-01 12:40:34 -04:00
85a7b58d55 abstracting out the slippage formula 2025-12-01 12:40:34 -04:00
tim
882e271040 updated Google Analytics tag 2025-11-26 16:52:51 -04:00
tim
a3053d1f7b Google Analytics 2025-11-26 15:31:13 -04:00
17 changed files with 861 additions and 376 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
.pnp.js .pnp.js
*secret*
.env
# testing # testing
/coverage /coverage

View File

@@ -0,0 +1,4 @@
cast call 0x8f98B899F4135408Fe03228cE942Ad6BF8E40f22 \
"working(address)" \
0x3EDE1eE859A72aEc85ff04d305B6Ffe89f2Cb4eb \
--rpc-url https://eth-sepolia.g.alchemy.com/v2/demo

80
package-lock.json generated
View File

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

View File

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

View File

@@ -139,13 +139,13 @@ const TEST_TOKENS = currentConfig.tokens;
// Default pool parameters // Default pool parameters
const DEFAULT_POOL_PARAMS = { const DEFAULT_POOL_PARAMS = {
name: 'Liquidity Party POC', name: 'Original Genesis of Liquidity Party',
symbol: 'POC.LP', symbol: 'OG.LP',
kappa: ethers.BigNumber.from('184467440737095520'), //0.01 * 2^64 kappa: ethers.BigNumber.from('184467440737095520'), //0.01 * 2^64
swapFeesPpm: Object.values(TEST_TOKENS).map(t => t.feePpm), swapFeesPpm: Object.values(TEST_TOKENS).map(t => t.feePpm),
flashFeePpm: 5, // 0.0005% flashFeePpm: 5, // 0.0005%
stable: false, stable: false,
initialLpAmount: ethers.utils.parseUnits('100', 18) // 100 USD in 18 decimals initialLpAmount: ethers.utils.parseUnits('1', 18) // 100 USD in 18 decimals
}; };
// Input amount in USD // Input amount in USD
@@ -289,13 +289,13 @@ async function approveTokens(provider, tokenAmounts, receiverPrivateKey) {
console.log(` [~] Approving ${symbol} ${tokenInfo.address} ${approvalAmount} (1% buffer)...`); console.log(` [~] Approving ${symbol} ${tokenInfo.address} ${approvalAmount} (1% buffer)...`);
try { try {
// // USDT and some tokens require setting allowance to 0 before setting a new value // USDT and some tokens require setting allowance to 0 before setting a new value
// // Skip for BNB as it has a broken approve function // Skip for BNB as it has a broken approve function
// if (symbol !== 'BNB') { if (symbol == 'USDT') {
// const resetTx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, 0); const resetTx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, 0);
// await resetTx.wait(); await resetTx.wait();
// console.log(` [+] ${symbol} allowance reset to 0`); console.log(` [+] ${symbol} allowance reset to 0`);
// } }
const tx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, approvalAmount); const tx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, approvalAmount);
await tx.wait(); await tx.wait();
@@ -445,14 +445,14 @@ async function main() {
// Step 4: Check balances // Step 4: Check balances
await checkBalances(provider, wallet, tokenAmounts, RECEIVER_ADDRESS); await checkBalances(provider, wallet, tokenAmounts, RECEIVER_ADDRESS);
// // // Step 5: Approve tokens // // Step 5: Approve tokens
// if (NETWORK === 'mockchain' && currentConfig.receiverPrivateKey) { if (NETWORK === 'mockchain' && currentConfig.receiverPrivateKey) {
// // On mockchain, use receiver wallet for approvals // On mockchain, use receiver wallet for approvals
// await approveTokens(provider, tokenAmounts, currentConfig.receiverPrivateKey); await approveTokens(provider, tokenAmounts, currentConfig.receiverPrivateKey);
// } else if (NETWORK === 'mainnet') { } else if (NETWORK === 'mainnet') {
// // On mainnet, use the main wallet (payer and receiver are the same) // On mainnet, use the main wallet (payer and receiver are the same)
// await approveTokens(provider, tokenAmounts, PRIVATE_KEY); await approveTokens(provider, tokenAmounts, PRIVATE_KEY);
// } }
// Step 6: Create pool // Step 6: Create pool
await createPool(wallet, tokenAmounts); await createPool(wallet, tokenAmounts);

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import '@rainbow-me/rainbowkit/styles.css';
import '@/app/globals.css'; import '@/app/globals.css';
import { Providers } from '@/components/providers'; import { Providers } from '@/components/providers';
import { Metadata } from 'next'; import { Metadata } from 'next';
import Script from "next/script";
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL('https://liquidity.party'), metadataBase: new URL('https://liquidity.party'),
@@ -37,6 +38,19 @@ export default function RootLayout({
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className="min-h-screen"> <body className="min-h-screen">
{/* Google tag (gtag.js) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-GH2R6NTLC3"
strategy="afterInteractive"
/>
<Script id="gtag-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){window.dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-GH2R6NTLC3');
`}
</Script>
<Providers>{children}</Providers> <Providers>{children}</Providers>
</body> </body>
</html> </html>

View File

@@ -20,6 +20,38 @@ interface StakeFormProps {
defaultMode?: Mode; 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 { interface TokenInfo {
address: `0x${string}`; address: `0x${string}`;
symbol: string; symbol: string;
@@ -141,19 +173,49 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
}, [stakeAmount, selectedToken, mode]); }, [stakeAmount, selectedToken, mode]);
// Fetch swap mint amounts (for stake 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' ? selectedPool?.address : undefined,
mode === 'stake' ? inputTokenIndex : 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) // 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 ? selectedPool?.address : undefined,
mode === 'unstake' && !redeemAll ? maxAmountIn : undefined, mode === 'unstake' && !redeemAll ? maxAmountIn : undefined,
mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined,
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]);
// 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) // Fetch burn amounts (for unstake mode when redeeming all)
const { burnAmounts, loading: burnAmountsLoading } = useBurnAmounts( const { burnAmounts, loading: burnAmountsLoading } = useBurnAmounts(
mode === 'unstake' && redeemAll ? selectedPool?.address : undefined, mode === 'unstake' && redeemAll ? selectedPool?.address : undefined,
@@ -664,6 +726,58 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
</div> </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) */} {/* Burn Swap Amounts Display (Unstake Mode) */}
{mode === 'unstake' && !redeemAll && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && ( {mode === 'unstake' && !redeemAll && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && (
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2"> <div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
@@ -712,10 +826,10 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
!selectedPool || !selectedPool ||
isAmountExceedingBalance || isAmountExceedingBalance ||
(mode === 'stake' (mode === 'stake'
? (!selectedToken || isSwapMinting) ? (!selectedToken || isSwapMinting || slippageExceedsLimit || !!swapMintError)
: (redeemAll : (redeemAll
? isBurning ? isBurning
: (!selectedToken || inputTokenIndex === undefined || isBurnSwapping))) : (!selectedToken || inputTokenIndex === undefined || isBurnSwapping || unstakeSlippageExceedsLimit || !!burnSwapError)))
} }
> >
{!isConnected {!isConnected

View File

@@ -6,17 +6,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ArrowDownUp, ChevronDown, Settings, CheckCircle, XCircle, Loader2 } from 'lucide-react'; 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 { useTokenDetails, useGetPoolsByToken, type TokenDetails } from '@/hooks/usePartyPlanner';
import { useSwapAmounts, useSwap, selectBestSwapRoute, type ActualSwapAmounts } from '@/hooks/usePartyPool'; import { useSwapAmounts, useSwap, selectBestSwapRoute, type ActualSwapAmounts } from '@/hooks/usePartyPool';
import { formatUnits, parseUnits } from 'viem'; import { formatUnits, parseUnits } from 'viem';
import { SwapReviewModal } from './swap-review-modal'; import { SwapReviewModal } from './swap-review-modal';
import UniswapQuote from './uniswap-quote';
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error'; type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
export function SwapForm() { export function SwapForm() {
const { t } = useTranslation(); const { t } = useTranslation();
const { isConnected, address } = useAccount(); const { isConnected, address } = useAccount();
const chainId = useChainId();
const [fromAmount, setFromAmount] = useState(''); const [fromAmount, setFromAmount] = useState('');
const [toAmount, setToAmount] = useState(''); const [toAmount, setToAmount] = useState('');
const [selectedFromToken, setSelectedFromToken] = useState<TokenDetails | null>(null); const [selectedFromToken, setSelectedFromToken] = useState<TokenDetails | null>(null);
@@ -73,6 +75,14 @@ export function SwapForm() {
} }
}, [selectedFromToken, fromAmount]); }, [selectedFromToken, fromAmount]);
// Check if calculated slippage exceeds 5%
const slippageExceedsLimit = useMemo(() => {
if (!swapAmounts || swapAmounts.length === 0 || swapAmounts[0].calculatedSlippage === undefined) {
return false;
}
return Math.abs(swapAmounts[0].calculatedSlippage) > 5;
}, [swapAmounts]);
// Initialize swap hook // Initialize swap hook
const { executeSwap, estimateGas, isSwapping, gasEstimate, isEstimatingGas } = useSwap(); const { executeSwap, estimateGas, isSwapping, gasEstimate, isEstimatingGas } = useSwap();
@@ -86,6 +96,8 @@ export function SwapForm() {
const swapResult = swapAmounts[0]; // Get the first (and should be only) result const swapResult = swapAmounts[0]; // Get the first (and should be only) result
const formattedAmount = formatUnits(swapResult.amountOut, selectedToToken.decimals); const formattedAmount = formatUnits(swapResult.amountOut, selectedToToken.decimals);
setToAmount(formattedAmount); setToAmount(formattedAmount);
} else {
setToAmount('');
} }
}, [swapAmounts, selectedToToken, hasInsufficientBalance]); }, [swapAmounts, selectedToToken, hasInsufficientBalance]);
@@ -301,6 +313,7 @@ export function SwapForm() {
onChange={(e) => setToAmount(e.target.value)} onChange={(e) => setToAmount(e.target.value)}
className="text-2xl h-16" className="text-2xl h-16"
disabled={!selectedFromToken} disabled={!selectedFromToken}
readOnly
/> />
<div className="relative min-w-[160px] space-y-1" ref={toDropdownRef}> <div className="relative min-w-[160px] space-y-1" ref={toDropdownRef}>
<Button <Button
@@ -370,18 +383,42 @@ export function SwapForm() {
</div> </div>
)} )}
{/* High slippage warning - show if calculated slippage exceeds max slippage OR 5% */} {/* Error message for slippage exceeding 5% */}
{swapAmounts && swapAmounts.length > 0 && swapAmounts[0].calculatedSlippage !== undefined && ( {slippageExceedsLimit && (
Math.abs(swapAmounts[0].calculatedSlippage) > Math.max(currentSlippage, 5) <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>
<div className="px-4 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"> <p className="text-xs text-destructive/80 mt-1">
<p className="text-sm text-yellow-600 dark:text-yellow-500 font-medium"> High Slippage Warning</p> The estimated slippage for this swap is {Math.abs(swapAmounts![0].calculatedSlippage!).toFixed(2)}%.
<p className="text-xs text-yellow-600/80 dark:text-yellow-500/80 mt-1"> We cannot process this swap as you may lose too much money due to the high slippage.
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.
</p> </p>
</div> </div>
)} )}
{/* High slippage warning - show if calculated slippage exceeds max slippage but is under 5% */}
{!slippageExceedsLimit && swapAmounts && swapAmounts.length > 0 && swapAmounts[0].calculatedSlippage !== undefined && (
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"> 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)}%, 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 */} {/* Gas Estimate, Slippage, and Fees */}
{isConnected && fromAmount && toAmount && ( {isConnected && fromAmount && toAmount && (
<div className="px-4 py-2 bg-muted/30 rounded-lg space-y-2"> <div className="px-4 py-2 bg-muted/30 rounded-lg space-y-2">
@@ -420,12 +457,14 @@ export function SwapForm() {
<Button <Button
className="w-full h-14 text-lg" className="w-full h-14 text-lg"
onClick={() => setIsReviewModalOpen(true)} onClick={() => setIsReviewModalOpen(true)}
disabled={!isConnected || !fromAmount || !toAmount || !!poolsError || hasInsufficientBalance} disabled={!isConnected || !fromAmount || !toAmount || !!poolsError || hasInsufficientBalance || slippageExceedsLimit}
> >
{!isConnected {!isConnected
? t('swap.connectWalletToSwap') ? t('swap.connectWalletToSwap')
: hasInsufficientBalance : hasInsufficientBalance
? 'Insufficient Balance' ? 'Insufficient Balance'
: slippageExceedsLimit
? 'Slippage Too High'
: 'Review'} : 'Review'}
</Button> </Button>
</CardContent> </CardContent>
@@ -469,7 +508,7 @@ export function SwapForm() {
</span> </span>
</div> </div>
<p className="text-xs text-muted-foreground mt-2"> <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> </p>
</div> </div>
</div> </div>

View File

@@ -4,18 +4,16 @@ export default function TosCard() {
return ( return (
<div className="max-w-5xl mx-auto p-5"> <div className="max-w-5xl mx-auto p-5">
<div className="bg-card rounded-lg shadow-md p-6 border"> <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 */} {/* MAKE SURE TO UPDATE THE VERSION VARIABLE AS WELL */}
<p className="text-center mb-4">Last Updated November 18, 2024</p> <p className="text-center mb-4">Last Updated November 18, 2024</p>
<div className="mb-4 leading-relaxed"> <div className="mb-4 leading-relaxed">
Please read these Terms of Service (the "<b>Terms</b>") and our{' '} Please read these Terms of Service (the "<b>Terms</b>")
<a href="https://dexorder.com/privacy-policy" target="dexorder">
Privacy Policy
</a>{' '}
carefully because they govern your use of the website (and all subdomains and subpages carefully because they govern your use of the website (and all subdomains and subpages
thereon) located at dexorder.com, including without limitation the subdomains thereon) located at liquidity.party, including without limitation the subdomains
app.dexorder.com and www.dexorder.com (collectively, the "<b>Site</b>"), and the Dexorder 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 web application graphical user interface and any other services accessible via the Site
(together with the Site, web application, and other services, collectively, the " (together with the Site, web application, and other services, collectively, the "
<b>Dexorder Service</b>") offered by Dexorder Trading Services Ltd. ("<b>Dexorder</b>," " <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"> <div className="mb-4 leading-relaxed">
(a) The Dexorder Service allows you to access an online web application graphical user (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 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 set of smart contracts (the "<b>Protocol</b>").
user controlled smart contract involving digital assets over which only you have upgrade You may use the <b>App</b> to send signals to, interact with, and initiate actions on the
authority (a "<b>Vault</b>" and together with the Smart Contracts, the "<b>Protocol</b>"). decentralized exchange ("<b>DEX</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.
</div> </div>
<div className="mb-4 leading-relaxed"> <div className="mb-4 leading-relaxed">
(b) <b>Interface</b>. The Dexorder Service provides you with access to the Protocol, which (b) <b>Interface</b>. The Dexorder Service provides you with access to the Protocol.
is a user controlled, non-custodial protocol, upgradeable only by your actions and consent, All information provided in
deployed on the blockchains indicated on our Site, and provides information and interaction
capabilities with other blockchain related service providers. All information provided in
connection with your access and use of the Dexorder Service is for informational purposes 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 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 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"> <div className="mb-4 leading-relaxed ml-8">
(i) Subject to your compliance with these Terms, Dexorder will use its commercially (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 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 Interactions to be executed on the Protocol, however from time to time the Site and
Policy located at{' '} the Dexorder Service may
<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
be inaccessible or inoperable for any reason, including, without limitation: (a) if an 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 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) 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 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 Dexorder could not reasonably foresee; (e) disruptions and temporary or permanent
unavailability of underlying blockchain infrastructure; (f) unavailability of third-party 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 service providers or external partners for any reason.
sent.
</div> </div>
<div className="mb-4 leading-relaxed ml-8"> <div className="mb-4 leading-relaxed ml-8">
(ii) the Site and the Dexorder Service may evolve, which means Dexorder may apply changes, (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> <h2 className="text-xl font-semibold mt-6 mb-4">6. Interactions; Fees</h2>
<div className="mb-4 leading-relaxed"> <div className="mb-4 leading-relaxed">
(a) <u>Interactions</u>. In order to effectuate Interactions, you may need to transfer (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 digital assets (e.g., tokens). You acknowledge that you may use the Dexorder
Services to process and cause Interactions to be operate with an applicable DEX, including Services to process and cause Interactions to operate on the Protocol.
without limitation the transfer of digital assets via the DEX in accordance with the Dexorder is an interface to that smart contract, and does not offer a digital
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
wallet and has no custody or control over your digital wallet or any digital assets or wallet and has no custody or control over your digital wallet or any digital assets or
cryptocurrency, which is never accessible by Dexorder. cryptocurrency, which is never accessible by Dexorder.
</div> </div>
@@ -257,8 +233,8 @@ export default function TosCard() {
(b) <u>Fees</u>. (b) <u>Fees</u>.
</div> </div>
<div className="mb-4 leading-relaxed ml-8"> <div className="mb-4 leading-relaxed ml-8">
(i) Dexorder charges fees upfront for usage of the Dexorder Services at the time of user (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 upfront to Dexorder, in 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 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 usage of the Dexorder Service. Each party shall be responsible for all Taxes imposed on its
income or property. 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> <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"> <div className="mb-4 leading-relaxed">
The Dexorder Service may allow you to access third-party websites, integrations, or other 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 resources, including the Protocol (collectively, "<b>Third Party Resources</b>"). We
the DEX and any third party protocols (collectively, "<b>Third Party Resources</b>"). We
provide access only as a convenience and are not responsible for the content, products or 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 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 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"> <div className="mb-4 leading-relaxed">
THE DEXORDER SERVICE (INCLUDING WITHOUT LIMITATION THE VAULT) AND ANY CONTENT CONTAINED 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 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 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 KIND. WITHOUT LIMITING THE FOREGOING, WE EXPLICITLY DISCLAIM ANY IMPLIED WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT AND NON-INFRINGEMENT, AND 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 USE OF VIRUSES, PHISHING, BRUTEFORCING OR OTHER MEANS OF ATTACK AGAINST ANY BLOCKCHAIN
NETWORK UNDERLYING THE UTILITIES. NETWORK UNDERLYING THE UTILITIES.
</div> </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"> <div className="mb-4 leading-relaxed">
THE UTILITIES MAY NOT BE AVAILABLE DUE TO ANY NUMBER OF FACTORS INCLUDING, BUT NOT LIMITED 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, 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": [ "outputs": [
{ {
"name": "", "name": "",
"type": "int128", "type": "uint256",
"internalType": "int128" "internalType": "uint256"
} }
], ],
"stateMutability": "view" "stateMutability": "view"

View File

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

View File

@@ -50,7 +50,7 @@ export function useGetAllTokens(offset: number = 0, limit: number = 100) {
// Get chain ID and contract address // Get chain ID and contract address
const chainId = await publicClient.getChainId(); const chainId = await publicClient.getChainId();
// @ts-ignore // @ts-ignore
const address = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyPoolViewer: string } }>)[chainId.toString()]?.v1?.PartyPlanner; const address = (chainInfo as Record<string, { v1: { PartyPlanner: string } }>)[chainId.toString()]?.v1?.PartyPlanner;
if (!address) { if (!address) {
setError('IPartyPlanner contract not found for current chain'); setError('IPartyPlanner contract not found for current chain');
@@ -99,6 +99,8 @@ export interface SwapRoute {
poolAddress: `0x${string}`; poolAddress: `0x${string}`;
inputTokenIndex: number; inputTokenIndex: number;
outputTokenIndex: number; outputTokenIndex: number;
inputTokenDecimal: number;
outputTokenDecimal: number;
} }
export interface AvailableToken { export interface AvailableToken {
@@ -196,68 +198,131 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs
return; 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 // Map to store available tokens with their swap routes
const tokenRoutesMap = new Map<string, AvailableToken>(); const tokenRoutesMap = new Map<string, AvailableToken>();
// For each working pool, fetch all tokens and track indices // For each working pool, process tokens
for (const poolAddress of workingPools) { for (let poolIdx = 0; poolIdx < workingPools.length; poolIdx++) {
try { const poolAddress = workingPools[poolIdx];
const tokensInPool = await publicClient.readContract({ const poolTokensResult = poolTokensResults[poolIdx];
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'allTokens',
}) as readonly `0x${string}`[];
// Find the input token index in this pool if (poolTokensResult.status !== 'success') {
const inputTokenIndex = tokensInPool.findIndex( console.error('Failed to fetch tokens for pool', poolAddress);
(token) => token.toLowerCase() === tokenAddress.toLowerCase() continue;
); }
if (inputTokenIndex === -1) { const tokensInPool = poolTokensResult.result as readonly `0x${string}`[];
console.error('Input token not found in pool', poolAddress);
// 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; continue;
} }
// Process each token in the pool const outputTokenData = tokenDataMap.get(outputTokenAddress.toLowerCase());
for (let outputTokenIndex = 0; outputTokenIndex < tokensInPool.length; outputTokenIndex++) { const outputTokenSymbol = outputTokenData?.symbol ?? null;
const outputTokenAddress = tokensInPool[outputTokenIndex]; const outputTokenDecimal = outputTokenData?.decimals ?? null;
// Skip if it's the same as the input token // Skip tokens with the same symbol as the selected token
if (outputTokenIndex === inputTokenIndex) { if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) {
continue; continue;
} }
// Get the symbol of this token // Skip tokens if decimals failed to load
const outputTokenSymbol = await publicClient.readContract({ 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, address: outputTokenAddress,
abi: ERC20ABI, symbol: outputTokenSymbol,
functionName: 'symbol', swapRoutes: [],
}).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,
}); });
} }
} 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,
});
} }
} }
@@ -304,55 +369,54 @@ export function useTokenDetails(userAddress: `0x${string}` | undefined) {
return; 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[] = []; const details: TokenDetails[] = [];
// Make individual calls for each token
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
const tokenAddress = tokens[i]; const baseIndex = i * 4;
try { const nameResult = results[baseIndex];
const [name, symbol, decimals, balance] = await Promise.all([ const symbolResult = results[baseIndex + 1];
publicClient.readContract({ const decimalsResult = results[baseIndex + 2];
address: tokenAddress, const balanceResult = results[baseIndex + 3];
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)),
]);
details.push({ details.push({
address: tokenAddress, address: tokens[i],
name: name as string, name: nameResult.status === 'success' ? (nameResult.result as string) : 'Unknown',
symbol: symbol as string, symbol: symbolResult.status === 'success' ? (symbolResult.result as string) : '???',
decimals: Number(decimals), decimals: decimalsResult.status === 'success' ? Number(decimalsResult.result) : 18,
balance: balance as bigint, balance: balanceResult.status === 'success' ? (balanceResult.result as bigint) : BigInt(0),
index: i, 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,
});
}
} }
setTokenDetails(details); setTokenDetails(details);
@@ -464,41 +528,12 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
let tvlStr: string | undefined; let tvlStr: string | undefined;
if (isWorking) { if (isWorking) {
// Fetch pool price (use first token as quote, index 0) // Fetch token decimals and balance first (needed for both price and TVL)
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)
try { try {
if (tokens && tokens.length > 0) { if (tokens && tokens.length > 0) {
const firstTokenAddress = tokens[0]; const firstTokenAddress = tokens[0];
// Get token decimals, balance, and pool price in parallel
// Get token decimals and balance const [decimals, balance, priceRaw] = await Promise.all([
const [decimals, balance] = await Promise.all([
publicClient.readContract({ publicClient.readContract({
address: firstTokenAddress as `0x${string}`, address: firstTokenAddress as `0x${string}`,
abi: ERC20ABI, abi: ERC20ABI,
@@ -510,16 +545,36 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
functionName: 'balanceOf', functionName: 'balanceOf',
args: [poolAddress], args: [poolAddress],
}) as Promise<bigint>, }) 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 // Calculate pool price using actual token decimals
const tokenBalance = Number(balance) / Math.pow(10, decimals); const price = BigInt(priceRaw as bigint | number);
const approximateTVL = tokenBalance * 3; 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); tvlStr = formatTVL(approximateTVL);
} }
} catch (err) { } catch (err) {
console.error(`Error fetching TVL for ${poolAddress}:`, err); console.error(`Error fetching pool price/TVL for ${poolAddress}:`, err);
priceStr = undefined;
tvlStr = undefined; tvlStr = undefined;
} }
} }
@@ -565,17 +620,20 @@ export interface SwapMintAmounts {
amountInUsed: bigint; amountInUsed: bigint;
fee: bigint; fee: bigint;
lpMinted: bigint; lpMinted: bigint;
calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5%
} }
export interface BurnSwapAmounts { export interface BurnSwapAmounts {
amountOut: bigint; amountOut: bigint;
outFee: bigint; outFee: bigint;
calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5%
} }
export function useSwapMintAmounts( export function useSwapMintAmounts(
poolAddress: `0x${string}` | undefined, poolAddress: `0x${string}` | undefined,
inputTokenIndex: number | undefined, inputTokenIndex: number | undefined,
maxAmountIn: bigint | undefined maxAmountIn: bigint | undefined,
inputTokenDecimals: number | undefined // Decimals of the input token
) { ) {
const publicClient = usePublicClient(); const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
@@ -596,8 +654,11 @@ export function useSwapMintAmounts(
} }
const fetchSwapMintAmounts = async () => { const fetchSwapMintAmounts = async () => {
if (!publicClient) { if (!publicClient) return;
setLoading(false);
// Early validation
if (inputTokenDecimals === undefined) {
setError('inputTokenDecimals is required but was undefined');
return; return;
} }
@@ -605,33 +666,58 @@ export function useSwapMintAmounts(
setLoading(true); setLoading(true);
setError(null); setError(null);
// Get chain ID and contract address
const chainId = await publicClient.getChainId(); const chainId = await publicClient.getChainId();
// @ts-ignore const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
const viewerAddress = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyPoolViewer: string } }>)[chainId.toString()]?.v1?.PartyPoolViewer;
if (!viewerAddress) { if (!partyInfoAddress) {
setError('IPartyPoolViewer contract not found for current chain'); setError('PartyInfo contract not found for current chain');
setSwapMintAmounts(null); setSwapMintAmounts(null);
return; return;
} }
// Call swapMintAmounts function
const result = await publicClient.readContract({ const result = await publicClient.readContract({
address: viewerAddress as `0x${string}`, address: partyInfoAddress as `0x${string}`,
abi: IPartyPoolViewerABI, abi: IPartyPoolViewerABI,
functionName: 'swapMintAmounts', functionName: 'swapMintAmounts',
args: [poolAddress, BigInt(inputTokenIndex), maxAmountIn], args: [poolAddress, BigInt(inputTokenIndex), maxAmountIn],
}) as readonly [bigint, bigint, bigint]; }) as readonly [bigint, bigint, bigint];
// Fetch and calculate pool price
let poolPrice: number | undefined;
let calculatedSlippage: number | undefined;
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({ setSwapMintAmounts({
amountInUsed: result[0], amountInUsed: result[0],
fee: result[1], fee: result[2],
lpMinted: result[2], lpMinted: result[1],
calculatedSlippage,
}); });
} catch (err) { } catch (err) {
console.error('Error calling swapMintAmounts:', 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); setSwapMintAmounts(null);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -639,7 +725,7 @@ export function useSwapMintAmounts(
}; };
fetchSwapMintAmounts(); fetchSwapMintAmounts();
}, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn]); }, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn, inputTokenDecimals]);
return { return {
swapMintAmounts, swapMintAmounts,
@@ -652,7 +738,8 @@ export function useSwapMintAmounts(
export function useBurnSwapAmounts( export function useBurnSwapAmounts(
poolAddress: `0x${string}` | undefined, poolAddress: `0x${string}` | undefined,
lpAmount: bigint | undefined, lpAmount: bigint | undefined,
inputTokenIndex: number | undefined inputTokenIndex: number | undefined,
tokenDecimals: number | undefined // Decimals of the output token
) { ) {
const publicClient = usePublicClient(); const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
@@ -681,58 +768,66 @@ export function useBurnSwapAmounts(
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
// Get chain ID and contract address // Get chain ID and contract address
const chainId = await publicClient.getChainId(); const chainId = await publicClient.getChainId();
// @ts-ignore // @ts-ignore
const viewerAddress = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyPoolViewer: string } }>)[chainId.toString()]?.v1?.PartyPoolViewer; const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!viewerAddress) { if (!partyInfoAddress) {
setError('IPartyPoolViewer contract not found for current chain'); setError('PartyInfo contract not found for current chain');
setBurnSwapAmounts(null); setBurnSwapAmounts(null);
return; return;
} }
// Log inputs
console.log('🔍 burnSwapAmounts INPUTS:', {
chainId: chainId.toString(),
rpcUrl: publicClient.transport?.url || 'Unknown',
poolAddress,
lpAmount: lpAmount.toString(),
inputTokenIndex,
viewerAddress,
});
// Call burnSwapAmounts function - returns [amountOut, outFee] // Call burnSwapAmounts function - returns [amountOut, outFee]
const result = await publicClient.readContract({ const result = await publicClient.readContract({
address: viewerAddress as `0x${string}`, address: partyInfoAddress as `0x${string}`,
abi: IPartyPoolViewerABI, abi: IPartyPoolViewerABI,
functionName: 'burnSwapAmounts', functionName: 'burnSwapAmounts',
args: [poolAddress, lpAmount, BigInt(inputTokenIndex)], args: [poolAddress, lpAmount, BigInt(inputTokenIndex)],
}) as readonly [bigint, bigint]; }) as readonly [bigint, bigint];
// Log raw result // Calculate slippage for burnSwap using poolPrice
console.log('📊 burnSwapAmounts RAW RESULT:', { let calculatedSlippage: number | undefined;
resultArray: result, if (tokenDecimals !== undefined) {
amountOut: result[0].toString(), try {
outFee: result[1].toString(), // Get the market price from poolPrice (quoteTokenIndex = 0)
}); 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)
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
const outAmountDecimal = Number(result[0]) / Math.pow(10, tokenDecimals);
const feeDecimal = Number(result[1]) / Math.pow(10, tokenDecimals);
const lpAmountDecimal = Number(lpAmount) / Math.pow(10, 18); // LP tokens have 18 decimals
const swapPrice = (outAmountDecimal + feeDecimal) / lpAmountDecimal;
calculatedSlippage = ((poolPrice - swapPrice) / poolPrice) * 100;
} catch (slippageErr) {
console.error(`Error calculating slippage for burnSwap:`, slippageErr);
}
}
const parsedAmounts = { const parsedAmounts = {
amountOut: result[0], amountOut: result[0],
outFee: result[1], outFee: result[1],
calculatedSlippage,
}; };
// Log parsed result
console.log('✅ burnSwapAmounts PARSED:', {
amountOut: parsedAmounts.amountOut.toString(),
outFee: parsedAmounts.outFee.toString(),
});
setBurnSwapAmounts(parsedAmounts); setBurnSwapAmounts(parsedAmounts);
} catch (err) { } catch (err) {
console.error('Error calling burnSwapAmounts:', 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); setBurnSwapAmounts(null);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -740,7 +835,7 @@ export function useBurnSwapAmounts(
}; };
fetchBurnSwapAmounts(); fetchBurnSwapAmounts();
}, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex]); }, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex, tokenDecimals]);
return { return {
burnSwapAmounts, burnSwapAmounts,

View File

@@ -11,6 +11,28 @@ import type { AvailableToken } from './usePartyPlanner';
// Q96 constant for price calculations // Q96 constant for price calculations
const Q96 = 1n << 96n; const Q96 = 1n << 96n;
/**
* Calculate slippage percentage based on market price and actual swap execution price
* @param marketPrice The current market price from the pool (in Q64 format, already converted to decimal)
* @param swapOutputAmount The output amount from the swap
* @param swapInputAmount The input amount for the swap
* @param swapFee The fee charged for the swap
* @returns Slippage as a percentage (e.g., 5.5 means 5.5%)
*/
export function calculateSlippage(
marketPrice: number,
swapOutputAmount: number,
swapInputAmount: number,
swapFee: number
): number {
// Calculate actual swap price with decimal correction
const swapPrice = swapOutputAmount / ((swapInputAmount) - (swapFee));
// Calculate slippage percentage: ((swapPrice - marketPrice) / marketPrice) * 100
const slippage = ((marketPrice - swapPrice) / marketPrice) * 100;
return slippage;
}
export interface SwapAmountResult { export interface SwapAmountResult {
tokenAddress: `0x${string}`; tokenAddress: `0x${string}`;
tokenSymbol: string; tokenSymbol: string;
@@ -98,18 +120,6 @@ export function useSwapAmounts(
setLoading(true); setLoading(true);
const amountInTokenUnits = parseUnits(fromAmount, fromTokenDecimals); 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 results: SwapAmountResult[] = [];
const chainId = await publicClient.getChainId(); const chainId = await publicClient.getChainId();
@@ -124,8 +134,8 @@ export function useSwapAmounts(
// Evaluate ALL routes for this token // Evaluate ALL routes for this token
for (const route of token.swapRoutes) { for (const route of token.swapRoutes) {
try { try {
// Get swap amounts // Get swap amounts with NO LIMIT
const swapResult = await publicClient.readContract({ const [swapInputAmount, swapOutputAmount, inFee] = await publicClient.readContract({
address: route.poolAddress, address: route.poolAddress,
abi: IPartyPoolABI, abi: IPartyPoolABI,
functionName: 'swapAmounts', functionName: 'swapAmounts',
@@ -133,12 +143,10 @@ export function useSwapAmounts(
BigInt(route.inputTokenIndex), BigInt(route.inputTokenIndex),
BigInt(route.outputTokenIndex), BigInt(route.outputTokenIndex),
amountInTokenUnits, amountInTokenUnits,
limitPrice, 0n, // NO LIMIT
], ],
}) as readonly [bigint, bigint, bigint]; }) as readonly [bigint, bigint, bigint];
const [amountIn, amountOut, fee] = swapResult;
// Get kappa for this pool // Get kappa for this pool
const kappa = await publicClient.readContract({ const kappa = await publicClient.readContract({
address: route.poolAddress, address: route.poolAddress,
@@ -150,19 +158,6 @@ export function useSwapAmounts(
let calculatedSlippage: number | undefined; let calculatedSlippage: number | undefined;
if (partyInfoAddress) { if (partyInfoAddress) {
try { 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 // Get the current market price from PoolInfo
const priceInt128 = await publicClient.readContract({ const priceInt128 = await publicClient.readContract({
address: partyInfoAddress, address: partyInfoAddress,
@@ -171,14 +166,17 @@ export function useSwapAmounts(
args: [route.poolAddress, BigInt(route.inputTokenIndex), BigInt(route.outputTokenIndex)], args: [route.poolAddress, BigInt(route.inputTokenIndex), BigInt(route.outputTokenIndex)],
}) as bigint; }) as bigint;
// Convert Q64 format to decimal (price = priceValue / 2^64) // Convert Q128 format to decimal (price = priceValue / 2^128)
const price = Number(priceInt128) / 2 ** 64; // 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
// Calculate actual swap price with decimal correction calculatedSlippage = calculateSlippage(marketPrice, swapOutputAmountDecimal, swapInputAmountDecimal, swapFeeDecimal);
const swapPrice = Number(swapOutputAmount) / ((Number(swapInputAmount)) - Number(inFee));
// Calculate slippage: 1 - (actualPrice / marketPrice)
const slippage = 1 - (swapPrice / price);
calculatedSlippage = slippage * 100; // Convert to percentage
console.log('calculatedSlippage', calculatedSlippage) console.log('calculatedSlippage', calculatedSlippage)
} catch (slippageErr) { } catch (slippageErr) {
console.error(`Error calculating slippage for ${token.symbol}:`, slippageErr); console.error(`Error calculating slippage for ${token.symbol}:`, slippageErr);
@@ -188,9 +186,9 @@ export function useSwapAmounts(
routeResults.push({ routeResults.push({
tokenAddress: token.address, tokenAddress: token.address,
tokenSymbol: token.symbol, tokenSymbol: token.symbol,
amountIn, amountIn: swapInputAmount,
amountOut, amountOut: swapOutputAmount,
fee, fee: inFee,
poolAddress: route.poolAddress, poolAddress: route.poolAddress,
kappa, kappa,
inputTokenIndex: route.inputTokenIndex, inputTokenIndex: route.inputTokenIndex,
@@ -362,9 +360,7 @@ export function useSwap() {
await publicClient.waitForTransactionReceipt({ hash: approvalHash }); await publicClient.waitForTransactionReceipt({ hash: approvalHash });
console.log('✅ Approval confirmed'); console.log('✅ Approval confirmed');
// STEP 2: Calculate limit price and deadline // STEP 2: Calculate deadline
const slippageBasisPoints = BigInt(Math.floor(slippagePercent * 100));
const limitPrice = (Q96 * (10000n + slippageBasisPoints)) / 10000n;
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200); const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200);
console.log('🚀 Executing swap with params:', { console.log('🚀 Executing swap with params:', {
@@ -374,12 +370,12 @@ export function useSwap() {
inputTokenIndex, inputTokenIndex,
outputTokenIndex, outputTokenIndex,
maxAmountIn: maxAmountIn.toString(), maxAmountIn: maxAmountIn.toString(),
limitPrice: limitPrice.toString(), limitPrice: '0 (no limit)',
deadline: deadline.toString(), deadline: deadline.toString(),
unwrap: false, unwrap: false,
}); });
// STEP 3: Execute the swap transaction // STEP 3: Execute the swap transaction with no limit price
const hash = await walletClient.writeContract({ const hash = await walletClient.writeContract({
address: poolAddress, address: poolAddress,
abi: IPartyPoolABI, abi: IPartyPoolABI,
@@ -391,7 +387,7 @@ export function useSwap() {
BigInt(inputTokenIndex), BigInt(inputTokenIndex),
BigInt(outputTokenIndex), BigInt(outputTokenIndex),
maxAmountIn, maxAmountIn,
limitPrice, 0n, // no limit price
deadline, deadline,
false, // unwrap false, // unwrap
'0x', // cbData (empty bytes) '0x', // cbData (empty bytes)