Compare commits

...

82 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
c8c23a4f54 first pass on user alert for slippage (this does not include bug where we calculate neg slippage because of price issue) 2025-11-26 11:00:14 -04:00
8cc9d00521 adding insufficient balance error in the swap form 2025-11-26 11:00:14 -04:00
tim
519a22847b fixed docker platforrm selection 2025-11-24 18:04:31 -04:00
aeb90f2e0f removing server routes that are unused 2025-11-24 17:15:38 -04:00
d47f3d566a adding the terms and conditions link 2025-11-24 13:43:05 -04:00
4214f94ab8 adding the BVI footer 2025-11-24 13:22:09 -04:00
ea44e0d941 update create pool from prices script: 2025-11-24 12:04:07 -04:00
tim
5b450ab303 fixed ts strict errs 2025-11-21 18:37:41 -04:00
tim
2585873d8a bin/deploy doesn't generate contracts or need a chain id 2025-11-21 18:34:11 -04:00
107d2ae5c0 adding redeem only function for killed pools) 2025-11-19 15:43:10 -04:00
66d854fb75 adding cbData param 2025-11-19 14:55:09 -04:00
d209742127 adding secret variables and tresure code 2025-11-19 14:37:33 -04:00
tim
ce4e2eaef0 splash fix 2025-11-18 18:23:50 -04:00
tim
5fe5563871 generate-contracts bugfix 2025-11-18 11:13:43 -04:00
bd430411bd create pool from prices script update to use a cast command 2025-11-17 14:12:34 -04:00
tim
d5c40b9667 create_pool_from_prices uses generated ABI 2025-11-17 12:37:15 -04:00
c905217004 adding mobile responsive headers 2025-11-17 11:33:13 -04:00
1cb6c4852f updating folder location 2025-11-14 14:36:56 -04:00
1fa6d686f4 added working logic on swap and error handling 2025-11-11 19:01:14 -04:00
2fbebed014 adding token prices 2025-11-11 19:01:14 -04:00
fea441b4e7 adding prices of pools 2025-11-11 19:01:14 -04:00
732dfd7780 consolidate protocol fees and swap fees into one 2025-11-11 19:01:14 -04:00
c69f0a47de showing burn amounts in the redeem all function. ALso adding an isworking flag for staking and ustaking 2025-11-11 19:01:14 -04:00
dab02db690 passing byte4(0) into the swap function 2025-11-11 19:01:14 -04:00
e5bcc632e5 LP Test Plan
Live doc can be found at: https://docs.google.com/document/d/1rSClOQK2tJkGGADpmXNkXZHHAr_5EIAb0F_1NaEszsI/edit?tab=t.0#heading=h.tkhey6vjoddq
2025-11-11 19:47:22 +00:00
0586a09161 new contract naming update 2025-11-11 14:42:13 -04:00
6e5eca7543 moving redeem all functionality to stake form. Getting rid of unstake basket tab 2025-11-11 14:41:52 -04:00
9795d03493 updating the contracts files 2025-11-11 14:35:30 -04:00
4ecda7de14 adding script to create new pools based on realtime prices from coingeck 2025-11-07 15:03:08 -04:00
14bad3d11e cleaning up logs added for testing 2025-11-06 12:53:47 -04:00
f690799b50 adding unstake basket page 2025-11-06 12:39:16 -04:00
0a518d31f9 adding actual amounts and fees to confirmation pages for swap, stake and unstake 2025-11-05 14:31:56 -04:00
9707c4892b resetting maxAmount to what the user inputs into the input field 2025-11-04 14:36:24 -04:00
1d6ebadcb2 gas cost estimates for small values 2025-11-04 13:46:45 -04:00
ed6426fad7 updating calcuated fees formula 2025-11-03 22:31:02 -04:00
292f675039 updating the default slippage 2025-11-02 19:01:27 -04:00
tim
41d0336aed generate-contracts sepolia 2025-11-02 18:08:37 -04:00
a30c8f866c updating social card title 2025-10-29 13:43:58 -04:00
e08506e72c burn swap and page menu updates 2025-10-29 12:36:07 -04:00
66e28ed08d adding a stake form and approvals in both stake and swap form 2025-10-28 11:27:26 -04:00
dbfdfbd4ab adding metadatabase tag 2025-10-27 18:28:50 -04:00
a0ff77435b update the beta label 2025-10-27 18:15:47 -04:00
tim
6e18711de9 About page 2025-10-27 18:13:02 -04:00
tim
d1986b3c5f About page 2025-10-27 18:08:35 -04:00
tim
c319482a4c About page 2025-10-27 18:04:44 -04:00
6c3fdba8fc Add a beta label in the header 2025-10-27 16:16:02 -04:00
bf6bef6a58 Adding logo and description 2025-10-27 15:44:01 -04:00
78cb00f9f5 about page link edits 2025-10-26 17:05:17 -04:00
c9ce0d065e adding about section 2025-10-26 16:55:34 -04:00
tim
a1e73df30a sepolia deployment 2025-10-23 13:53:48 -04:00
380c77b37e updating theme provider 2025-10-23 11:33:10 -04:00
b5da8ce080 final touches on the swpa-from 2025-10-20 19:16:30 -04:00
caf6bff469 adding toast and displaying fees in the swap-form and modal 2025-10-20 18:09:16 -04:00
cdbf2a57e6 review modal, new smart contract pointer and network gas fees estimation 2025-10-20 16:45:43 -04:00
f543b27620 swap functionality 2025-10-16 16:57:10 -04:00
7ead103f86 [wip] adding swap amount conversion to the swam-form 2025-10-15 18:42:23 -04:00
e2198c9b31 adding token from pool logic and updating ABIs to support new smart contracts 2025-10-15 13:36:04 -04:00
64b998d119 slippage stuff 2025-10-14 18:26:36 -04:00
49 changed files with 6956 additions and 324 deletions

4
.gitignore vendored
View File

@@ -1,9 +1,13 @@
/src/contracts/liqp-deployments.json
/.idea/
# dependencies
/node_modules
/.pnp
.pnp.js
*secret*
.env
# testing
/coverage

View File

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

BIN
LP Test Plan.docx Normal file

Binary file not shown.

2
bin/build Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
BUILD=1 bin/deploy "$1"

43
bin/deploy Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/bash
if [ "$1" = "dev" ]; then
DEV=1
shift
else
DEV=0
fi
PROJECT=liquidity-party
REMOTE=git.dxod.org/dexorder/dexorder
if [ "$BUILD" != "1" ]; then
if [ "$DEV" = "1" ]; then
TAG="dev$(date +%Y%m%d%H%M%S)"
else
DIRTY="$( git status | grep "Changes " )"
if [ "$DIRTY" != "" ]; then
echo This project has uncommited changes: deployment aborted.
exit 1
fi
TAG="$( git log --oneline | head -1 | cut -d ' ' -f 1 )"
fi
echo Deploying $TAG
else
echo Building
fi
npm run build || exit 1
if [ "$BUILD" != "1" ]; then
echo Deploying with tag $TAG
docker buildx build --platform linux/amd64 --no-cache -f deploy/Dockerfile -t dexorder/$PROJECT:latest . || exit 1
docker tag dexorder/$PROJECT:latest dexorder/$PROJECT:$TAG
docker tag dexorder/$PROJECT:$TAG $REMOTE/$PROJECT:$TAG
docker tag $REMOTE/$PROJECT:$TAG $REMOTE/$PROJECT:latest
docker push $REMOTE/$PROJECT:$TAG
YAML=$(sed "s#image: dexorder/$PROJECT*#image: $REMOTE/$PROJECT:$TAG#" deploy/liquidity-party.k8s.yaml)
echo "$YAML" | kubectl apply -f - || echo "$YAML" "\nkubectl apply failed" && exit 1
echo "$(date)" deployed $REMOTE/$PROJECT:$TAG
else
echo "$(date)" built
fi

View File

@@ -1,11 +0,0 @@
#!/bin/sh
generate() {
ABI=$(jq '.abi' ../lmsr-amm/out/$1.sol/$1.json)
# echo "import {useReadContract} from \"wagmi\";\n\nconst {data} = useReadContract({abi: ${ABI}});\n\nexport default data;" > ./src/contracts/$1.ts
echo "/* GENERATED FILE: DO NOT EDIT! */\n\nconst ${1}ABI = ${ABI} as const;\n\nexport default ${1}ABI;" > ./src/contracts/$1ABI.ts
}
generate IPartyPlanner
generate IPartyPool
generate IPartyPoolViewer

43
bin/generate-contracts Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/sh
CHAIN_ID=${1:-31337}
case "$CHAIN_ID" in
"sepolia")
CHAIN_ID=11155111
;;
"mockchain")
CHAIN_ID=31337
;;
"mainnet")
CHAIN_ID=1
;;
esac
if [ "$CHAIN_ID" = "31337" ]; then
ABI_PATH=../lmsr-amm/out
METADATA_PATH=../lmsr-amm/liqp-deployments.json
else
ABI_PATH=../lmsr-amm/deployment/$CHAIN_ID/v1/out
METADATA_PATH=../lmsr-amm/deployment/liqp-deployments.json
fi
if [ ! -f "$ABI_PATH/IPartyPool.sol/IPartyPool.json" ]; then
echo "Invalid chain ID $CHAIN_ID"
exit 1
fi
echo Chain $CHAIN_ID
generate() {
ABI=$(jq '.abi' $ABI_PATH/$1.sol/$1.json)
echo "/* GENERATED FILE: DO NOT EDIT! */\n\nconst ${1}ABI = ${ABI} as const;\n\nexport default ${1}ABI;" > ./src/contracts/$1ABI.ts
echo "src/contracts/$1ABI.ts"
}
generate IPartyPlanner
generate IPartyPool
generate IPartyInfo
cp "$METADATA_PATH" src/contracts/
echo src/contracts/liqp-deployments.json

10
deploy/Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM nginx:stable-alpine
RUN apk update && apk upgrade
WORKDIR /liquidity.party/web
COPY out ./out
RUN sed -i '1idaemon off;' /etc/nginx/nginx.conf
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx"]

View File

@@ -0,0 +1,103 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: liquidity-party
labels:
app: liquidity-party
spec:
replicas: 1
selector:
matchLabels:
app: liquidity-party
template:
metadata:
labels:
app: liquidity-party
annotations:
prometheus.io/scrape: "false"
spec:
containers:
- name: liquidity-party
image: dexorder/liquidity-party
ports:
- name: www
containerPort: 80
protocol: TCP
resources:
requests:
cpu: 10m
memory: 100M
---
apiVersion: v1
kind: Service
metadata:
name: liquidity-party
labels:
app: liquidity-party
spec:
selector:
app: liquidity-party
ports:
- name: liquidity-party
port: 80
targetPort: 80
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: liquidity-party
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true" # Redirects HTTP to HTTPS
spec:
ingressClassName: nginx
tls:
- secretName: liquidity-party-tls
hosts:
- liquidity.party
rules:
- host: liquidity.party
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: liquidity-party
port:
number: 80
# www redirects to apex domain
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: www-liquidity-party
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true" # Redirects HTTP to HTTPS
nginx.ingress.kubernetes.io/permanent-redirect: "https://liquidity.party/"
spec:
ingressClassName: nginx
tls:
- secretName: www-liquidity-party-tls
hosts:
- www.liquidity.party
rules:
- host: www.liquidity.party
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: liquidity-party
port:
number: 80

23
deploy/nginx.conf Normal file
View File

@@ -0,0 +1,23 @@
server {
listen 80 default_server;
root /liquidity.party/web/out;
gzip on;
gzip_min_length 1000;
gzip_types text/plain text/xml application/javascript text/css;
location / {
try_files $uri $uri.html $uri/ /index.html;
}
# This is necessary when `trailingSlash: false`.
# You can omit this when `trailingSlash: true`.
location /blog/ {
rewrite ^/blog/(.*)$ /blog/$1.html break;
}
error_page 404 /404.html;
location = /404.html {
internal;
}
}

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

@@ -3,7 +3,7 @@
version="1.1"
viewBox="0 0 86.435997 86.435997"
id="svg585"
sodipodi:docname="logo-flower.svg"
sodipodi:docname="logo-splash.svg"
width="86.435997"
height="86.435997"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
@@ -27,24 +27,17 @@
inkscape:cx="218.22034"
inkscape:cy="48.516949"
inkscape:window-width="1864"
inkscape:window-height="1131"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg585" />
<!-- Flower -->
<g
fill="#000000"
id="g583"
transform="translate(-1.782,-1.782)">
<g
id="g581">
<circle
fill="#000000"
cx="45"
cy="44.999001"
r="5.277"
id="circle545" />
<g
id="g579">
<g

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
public/social-card-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1,476 @@
#!/usr/bin/env node
/**
* Create Pool from Real-Time Prices Script
* Fetches real-time prices from CoinGecko and creates a new pool on the chain specified (could be mockchain, sepolia or prod)
*/
import { ethers } from 'ethers';
import { readFile } from 'fs/promises';
import { config } from 'dotenv';
// Load environment variables from .env-secret
config({ path: new URL('../.env-secret', import.meta.url).pathname });
// ============================================================================
// CONFIGURATION
// ============================================================================
// Network flag: 'mockchain' or 'mainnet'
const NETWORK = process.env.NETWORK || 'mainnet';
// Network-specific configuration
const NETWORK_CONFIG = {
mockchain: {
rpcUrl: 'http://localhost:8545',
privateKey: '0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a', // Dev wallet #4 (sender)
receiverPrivateKey: '0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356', // Receiver wallet
receiverAddress: '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955', // Receiver public key
chainId: '31337',
tokens: {
USDT: {
address: '0xbdEd0D2bf404bdcBa897a74E6657f1f12e5C6fb6', // USXD on mockchain
coingeckoId: 'tether',
decimals: 6,
feePpm: 400
},
USDC: {
address: '0x2910E325cf29dd912E3476B61ef12F49cb931096', // FUSD on mockchain
coingeckoId: 'usd-coin',
decimals: 6,
feePpm: 400
}
}
},
mainnet: {
rpcUrl: process.env.MAINNET_RPC_URL,
privateKey: process.env.PRIVATE_KEY,
receiverAddress: '0xd3b310bd32d782f89eea49cb79656bcaccde7213', // Same as payer for mainnet
chainId: '1',
tokens: {
USDT: {
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
coingeckoId: 'tether',
decimals: 6,
feePpm: 40 // 0.0004%
},
USDC: {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
coingeckoId: 'usd-coin',
decimals: 6,
feePpm: 40 // 0.0004%
},
WBTC: {
address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
coingeckoId: 'wrapped-bitcoin',
decimals: 8,
feePpm: 300 // 0.00030%
},
WETH: {
address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
coingeckoId: 'weth',
decimals: 18,
feePpm: 350 // 0.0035%
},
UNI: {
address: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984',
coingeckoId: 'uniswap',
decimals: 18,
feePpm: 1450 // 0.00145%
},
WSOL: {
address: '0xD31a59c85aE9D8edEFeC411D448f90841571b89c', // Wormhole Wrapped SOL
coingeckoId: 'solana',
decimals: 9,
feePpm: 950 // 0.00095%
},
TRX: {
address: '0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5',
coingeckoId: 'tron',
decimals: 6,
feePpm: 950 // 0.00095%
},
AAVE: {
address: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9',
coingeckoId: 'aave',
decimals: 18,
feePpm: 1450 // 0.00145%
},
PEPE: {
address: '0x6982508145454Ce325dDbE47a25d4ec3d2311933',
coingeckoId: 'pepe',
decimals: 18,
feePpm: 2150 // 0.00215%
},
SHIB: {
address: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE',
coingeckoId: 'shiba-inu',
decimals: 18,
feePpm: 2150 // 0.00215%
}
}
}
};
// Get current network config
const currentConfig = NETWORK_CONFIG[NETWORK];
if (!currentConfig) {
console.error(`[!] Invalid NETWORK: ${NETWORK}. Must be 'mockchain' or 'mainnet'`);
process.exit(1);
}
const RPC_URL = currentConfig.rpcUrl;
const PRIVATE_KEY = currentConfig.privateKey;
const RECEIVER_ADDRESS = currentConfig.receiverAddress || process.env.RECEIVER_ADDRESS;
// Validate required config for mainnet
if (NETWORK === 'mainnet' && (!RPC_URL || !PRIVATE_KEY)) {
console.error('[!] Missing required environment variables for mainnet');
console.error(' Required: MAINNET_RPC_URL, PRIVATE_KEY');
process.exit(1);
}
if (!RECEIVER_ADDRESS) {
console.error('[!] Missing RECEIVER_ADDRESS in .env-secret file');
process.exit(1);
}
// Use network-specific tokens
const TEST_TOKENS = currentConfig.tokens;
// Default pool parameters
const DEFAULT_POOL_PARAMS = {
name: 'Original Genesis of Liquidity Party',
symbol: 'OG.LP',
kappa: ethers.BigNumber.from('184467440737095520'), //0.01 * 2^64
swapFeesPpm: Object.values(TEST_TOKENS).map(t => t.feePpm),
flashFeePpm: 5, // 0.0005%
stable: false,
initialLpAmount: ethers.utils.parseUnits('1', 18) // 100 USD in 18 decimals
};
// Input amount in USD
const INPUT_USD_AMOUNT = 1;
// ============================================================================
// LOAD ABIs AND CONFIG
// ============================================================================
const chainInfoData = JSON.parse(await readFile(new URL('../src/contracts/liqp-deployments.json', import.meta.url), 'utf-8'));
const PARTY_PLANNER_ADDRESS = chainInfoData[currentConfig.chainId].v1.PartyPlanner;
const ERC20ABI = [
{ "type": "function", "name": "balanceOf", "stateMutability": "view", "inputs": [{ "name": "account", "type": "address" }], "outputs": [{ "name": "", "type": "uint256" }] },
{ "type": "function", "name": "decimals", "stateMutability": "view", "inputs": [], "outputs": [{ "name": "", "type": "uint8" }] },
{ "type": "function", "name": "approve", "stateMutability": "nonpayable", "inputs": [{ "name": "spender", "type": "address" }, { "name": "amount", "type": "uint256" }], "outputs": [{ "name": "", "type": "bool" }] }
];
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Fetch real-time prices from CoinGecko API
*/
async function fetchCoinGeckoPrices() {
try {
const ids = Object.values(TEST_TOKENS).map(t => t.coingeckoId).join(',');
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`;
console.log(`[~] Fetching prices from CoinGecko...`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`CoinGecko API request failed: ${response.statusText}`);
}
const data = await response.json();
const prices = {};
for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) {
prices[symbol] = data[tokenInfo.coingeckoId]?.usd || 0;
if (prices[symbol] === 0) {
throw new Error(`Failed to fetch valid price for ${symbol}`);
}
}
console.log(`[+] Prices fetched successfully:`);
for (const [symbol, price] of Object.entries(prices)) {
console.log(` ${symbol.padEnd(6)}: $${price.toLocaleString()}`);
}
return prices;
} catch (error) {
console.error(`[!] Error fetching prices:`, error.message);
throw error;
}
}
/**
* Calculate token amounts based on equal USD distribution
*/
function calculateTokenAmounts(prices, usdAmount) {
const tokenCount = Object.keys(TEST_TOKENS).length;
const usdPerToken = usdAmount / tokenCount; // Equally distribute among all tokens
const tokenAmounts = {};
console.log(`\n[~] Calculated token amounts for $${usdAmount} ($${usdPerToken.toFixed(2)} per token):`);
for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) {
// Calculate raw amount
const rawAmount = usdPerToken / prices[symbol];
// Convert to BigNumber with proper decimals
const amountBN = ethers.utils.parseUnits(rawAmount.toFixed(tokenInfo.decimals), tokenInfo.decimals);
tokenAmounts[symbol] = amountBN;
console.log(` ${symbol.padEnd(6)}: ${rawAmount.toFixed(tokenInfo.decimals)} (${amountBN.toString()} wei)`);
}
return tokenAmounts;
}
/**
* Check token balances
*/
async function checkBalances(provider, wallet, tokenAmounts, receiverAddress) {
console.log(`\n[~] Checking token balances for receiver wallet: ${receiverAddress}`);
const balances = {};
let hasEnoughBalance = true;
for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) {
const tokenContract = new ethers.Contract(tokenInfo.address, ERC20ABI, provider);
const balance = await tokenContract.balanceOf(receiverAddress);
const requiredAmount = tokenAmounts[symbol];
balances[symbol] = balance;
const balanceFormatted = ethers.utils.formatUnits(balance, tokenInfo.decimals);
const requiredFormatted = ethers.utils.formatUnits(requiredAmount, tokenInfo.decimals);
const sufficient = balance.gte(requiredAmount);
console.log(` ${symbol}: ${balanceFormatted} (required: ${requiredFormatted}) ${sufficient ? '✓' : '✗'}`);
if (!sufficient) {
hasEnoughBalance = false;
}
}
if (!hasEnoughBalance) {
console.log(`\n[!] Insufficient token balance. Please ensure receiver wallet has enough tokens.`);
throw new Error('Insufficient token balance');
}
console.log(`[+] All balances sufficient`);
return balances;
}
/**
* Approve tokens for the PartyPlanner contract
*/
async function approveTokens(provider, tokenAmounts, receiverPrivateKey) {
console.log(`\n[~] Approving tokens for PartyPlanner contract...`);
// Connect with receiver wallet for approvals
const receiverWallet = new ethers.Wallet(receiverPrivateKey, provider);
console.log(`[~] Using receiver wallet for approvals: ${receiverWallet.address}`);
for (const [symbol, tokenInfo] of Object.entries(TEST_TOKENS)) {
const tokenContract = new ethers.Contract(tokenInfo.address, ERC20ABI, receiverWallet);
// Approve 1% more than needed to account for fees/slippage
const requiredAmount = tokenAmounts[symbol];
const approvalAmount = requiredAmount.mul(101).div(100); // 1% buffer
console.log(` [~] Approving ${symbol} ${tokenInfo.address} ${approvalAmount} (1% buffer)...`);
try {
// USDT and some tokens require setting allowance to 0 before setting a new value
// Skip for BNB as it has a broken approve function
if (symbol == 'USDT') {
const resetTx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, 0);
await resetTx.wait();
console.log(` [+] ${symbol} allowance reset to 0`);
}
const tx = await tokenContract.approve(PARTY_PLANNER_ADDRESS, approvalAmount);
await tx.wait();
console.log(` [+] ${symbol} approved (tx: ${tx.hash})`);
} catch (error) {
console.error(` [!] Failed to approve ${symbol}:`, error.message);
throw error;
}
}
console.log(`[+] All tokens approved`);
}
/**
* Create a new pool using cast send
*/
async function createPool(wallet, tokenAmounts) {
console.log(`\n[~] Creating new pool...`);
// Prepare parameters
const tokenAddresses = Object.values(TEST_TOKENS).map(t => t.address);
const initialDeposits = Object.keys(TEST_TOKENS).map(symbol => tokenAmounts[symbol].toString());
// Set deadline to 5 minutes from now
const deadline = Math.floor(Date.now() / 1000) + 300;
console.log(`[~] Pool parameters:`);
console.log(` Name: ${DEFAULT_POOL_PARAMS.name}`);
console.log(` Symbol: ${DEFAULT_POOL_PARAMS.symbol}`);
console.log(` Tokens: ${tokenAddresses.join(', ')}`);
console.log(` Swap Fees PPM: [${DEFAULT_POOL_PARAMS.swapFeesPpm.join(', ')}]`);
console.log(` Payer (provides tokens): ${RECEIVER_ADDRESS}`);
console.log(` Receiver (gets LP tokens): ${wallet.address}`);
console.log(` Deadline: ${new Date(deadline * 1000).toISOString()}`);
// Build cast send command
const castCommand = `cast send ${PARTY_PLANNER_ADDRESS} \
"newPool(string,string,address[],int128,uint256[],uint256,bool,address,address,uint256[],uint256,uint256)" \
"${DEFAULT_POOL_PARAMS.name}" \
"${DEFAULT_POOL_PARAMS.symbol}" \
"[${tokenAddresses.join(',')}]" \
${DEFAULT_POOL_PARAMS.kappa.toString()} \
"[${DEFAULT_POOL_PARAMS.swapFeesPpm.join(',')}]" \
${DEFAULT_POOL_PARAMS.flashFeePpm} \
${DEFAULT_POOL_PARAMS.stable} \
${RECEIVER_ADDRESS} \
${wallet.address} \
"[${initialDeposits.join(',')}]" \
${DEFAULT_POOL_PARAMS.initialLpAmount.toString()} \
${deadline} \
--rpc-url '${RPC_URL}' \
--from 0x12db90820dafed100e40e21128e40dcd4ff6b331 \
--trezor --mnemonic-index 0`
console.log(`\n[~] Cast command:\n${castCommand}\n`);
try {
// Execute cast command
const { execSync } = await import('child_process');
const output = execSync(castCommand, { encoding: 'utf-8' });
console.log(`[+] Pool created successfully!`);
console.log(output);
return output;
} catch (error) {
console.error(`[!] Failed to create pool:`, error.message);
if (error.stderr) {
console.error(` Error output: ${error.stderr.toString()}`);
}
throw error;
}
}
/**
* Print help message
*/
function printHelp() {
console.log(`
Usage: node create_pool_from_prices.js [OPTIONS]
Options:
--amount <usd> USD amount to distribute (default: ${INPUT_USD_AMOUNT})
--name <name> Pool name (default: "${DEFAULT_POOL_PARAMS.name}")
--symbol <symbol> Pool symbol (default: "${DEFAULT_POOL_PARAMS.symbol}")
--help, -h Show this help message
Example:
node create_pool_from_prices.js
node create_pool_from_prices.js --amount 200 --name "My Pool" --symbol "MP"
`);
}
// ============================================================================
// MAIN FUNCTION
// ============================================================================
async function main() {
console.log(`${'='.repeat(70)}`);
console.log(`Create Pool from Real-Time Prices`);
console.log(`Network: ${NETWORK}`);
console.log(`${'='.repeat(70)}\n`);
// Parse command line arguments
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
printHelp();
process.exit(0);
}
let usdAmount = INPUT_USD_AMOUNT;
let poolName = DEFAULT_POOL_PARAMS.name;
let poolSymbol = DEFAULT_POOL_PARAMS.symbol;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--amount' && i + 1 < args.length) {
usdAmount = parseFloat(args[i + 1]);
i++;
} else if (args[i] === '--name' && i + 1 < args.length) {
poolName = args[i + 1];
i++;
} else if (args[i] === '--symbol' && i + 1 < args.length) {
poolSymbol = args[i + 1];
i++;
}
}
// Update pool params with parsed values
DEFAULT_POOL_PARAMS.name = poolName;
DEFAULT_POOL_PARAMS.symbol = poolSymbol;
try {
// Step 1: Fetch prices
const prices = await fetchCoinGeckoPrices();
// Step 2: Calculate token amounts
const tokenAmounts = calculateTokenAmounts(prices, usdAmount);
//
// // Step 3: Connect to wallet
console.log(`\n[~] Connecting to sender wallet at ${RPC_URL}...`);
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
console.log(`[+] Connected. Using sender wallet: ${wallet.address}`);
console.log(`[+] Receiver wallet: ${RECEIVER_ADDRESS}`);
//
// Step 4: Check balances
await checkBalances(provider, wallet, tokenAmounts, RECEIVER_ADDRESS);
// // Step 5: Approve tokens
if (NETWORK === 'mockchain' && currentConfig.receiverPrivateKey) {
// On mockchain, use receiver wallet for approvals
await approveTokens(provider, tokenAmounts, currentConfig.receiverPrivateKey);
} else if (NETWORK === 'mainnet') {
// On mainnet, use the main wallet (payer and receiver are the same)
await approveTokens(provider, tokenAmounts, PRIVATE_KEY);
}
// Step 6: Create pool
await createPool(wallet, tokenAmounts);
console.log(`\n${'='.repeat(70)}`);
console.log(`Success! Pool created with real-time price-based deposits.`);
console.log(`${'='.repeat(70)}\n`);
} catch (error) {
console.error(`\n${'='.repeat(70)}`);
console.error(`[!] Error: ${error.message}`);
console.error(`${'='.repeat(70)}\n`);
process.exit(1);
}
}
// Run the main function
main().catch(error => {
console.error('[!] Unexpected error:', error);
process.exit(1);
});

14
scripts/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "liquidity-party-scripts",
"version": "1.0.0",
"description": "Standalone scripts for Liquidity Party DEX development and testing",
"type": "module",
"private": true,
"scripts": {
"create-pool": "node create_pool_from_prices.js"
},
"dependencies": {
"dotenv": "^17.2.3",
"ethers": "^5.7.2"
}
}

245
src/app/about/page.tsx Normal file
View File

@@ -0,0 +1,245 @@
'use client';
import { useTranslation } from 'react-i18next';
import { Card } from '@/components/ui/card';
import {useTheme} from "next-themes";
import {useState} from "react";
export default function AboutPage() {
const { t } = useTranslation();
const { theme, setTheme } = useTheme();
const logoSrc = theme === 'dark' ? '/logo-dark.svg' : '/logo-light.svg';
return (
<div className="w-full max-w-5xl mx-auto space-y-8">
{/* Header */}
<div className="text-center space-y-4">
<img
src={logoSrc}
alt="Liquidity Party Logo"
className="mx-auto h-24 w-auto"
/>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Multi-asset AMM based on Party Game Theory
</p>
</div>
{/* Introduction */}
<Card className="p-8 space-y-4">
<h2 className="text-2xl font-semibold">Introduction</h2>
<p className="text-muted-foreground leading-relaxed">
Liquidity Party is a new game-theoretic multi-asset AMM based on the groundbreaking paper:{' '}
<a
href="https://mason.gmu.edu/~rhanson/mktscore.pdf"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline italic"
>
Logarithmic Market Scoring Rules for Modular Combinatorial Information Aggregation (R. Hanson, 2002)
</a>.
</p>
<p className="text-muted-foreground leading-relaxed">
Our formulation and implementation is described in the{' '}
<a href="https://github.com/Liquidity-Party/lmsr-amm/blob/main/doc/whitepaper.md"
target='liqp_whitepaper'
rel="noopener noreferrer"
className="text-primary hover:underline italic"
>
Liquidity Party whitepaper.
</a>
</p>
<p className="text-muted-foreground leading-relaxed">
A Logarithmic Market Scoring Rule (LMSR) is a pricing formula for AMMs that know only their current asset
inventories and no other information, naturally supporting multi-asset pools.
</p>
</Card>
{/* Key Advantages */}
<Card className="p-8 space-y-4">
<h2 className="text-2xl font-semibold">Advantages Over Constant Product Markets</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="p-4 bg-muted rounded-lg">
<h3 className="font-semibold text-primary mb-2">Less Slippage</h3>
<p className="text-sm text-muted-foreground">
Reduced slippage for small and medium trade sizes
</p>
</div>
<div className="p-4 bg-muted rounded-lg">
<h3 className="font-semibold text-primary mb-2">Multi-Asset Pools</h3>
<p className="text-sm text-muted-foreground">
Trade long-tail pairs in a single hop with multi-asset support
</p>
</div>
<div className="p-4 bg-muted rounded-lg">
<h3 className="font-semibold text-primary mb-2">Lower Fees</h3>
<p className="text-sm text-muted-foreground">
Smaller spread means more cost-effective trading
</p>
</div>
<div className="p-4 bg-muted rounded-lg">
<h3 className="font-semibold text-primary mb-2">Deeper Liquidity</h3>
<p className="text-sm text-muted-foreground">
Enhanced liquidity depth for better trading execution
</p>
</div>
</div>
<p className="text-muted-foreground leading-relaxed mt-4">
According to game theory, the initial price slope of a Constant Product AMM is too steep, overcharging takers
with too much slippage at small and medium trade sizes. LMSR pools offer less slippage and cheaper
liquidity for the small and medium trade sizes used by real traders.
</p>
<div className="mt-6">
<img
src="/slippage-comparison.png"
alt="Slippage Comparison Chart"
className="w-full rounded-lg"
/>
</div>
</Card>
{/* Multi-Asset */}
<Card className="p-8 space-y-6">
<div className="space-y-4">
<h2 className="text-2xl font-semibold">Multi-Asset Capabilities</h2>
<p className="text-muted-foreground leading-relaxed">
Naturally multi-asset, Liquidity Party altcoin pools provide direct, one-hop swaps on otherwise
illiquid multi-hop pairs. Pools will quote any pair combination available in the pool:
</p>
</div>
{/* Performance Table */}
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b">
<th className="text-left p-3 font-semibold">Assets</th>
<th className="text-left p-3 font-semibold">Pairs</th>
<th className="text-left p-3 font-semibold">Swap Gas</th>
<th className="text-left p-3 font-semibold">Mint Gas</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr className="border-b hover:bg-muted/50 transition-colors">
<td className="p-3">2</td>
<td className="p-3">1</td>
<td className="p-3">131,000</td>
<td className="p-3">143,000</td>
</tr>
<tr className="border-b hover:bg-muted/50 transition-colors">
<td className="p-3">2*</td>
<td className="p-3">1</td>
<td className="p-3">118,000</td>
<td className="p-3">143,000</td>
</tr>
<tr className="border-b hover:bg-muted/50 transition-colors">
<td className="p-3">10</td>
<td className="p-3">45</td>
<td className="p-3">142,000</td>
<td className="p-3">412,000</td>
</tr>
<tr className="border-b hover:bg-muted/50 transition-colors">
<td className="p-3">20</td>
<td className="p-3">190</td>
<td className="p-3">157,000</td>
<td className="p-3">749,000</td>
</tr>
<tr className="border-b hover:bg-muted/50 transition-colors">
<td className="p-3">50</td>
<td className="p-3">1,225</td>
<td className="p-3">199,000</td>
<td className="p-3">1,760,000</td>
</tr>
<tr className="hover:bg-muted/50 transition-colors">
<td className="p-3">100</td>
<td className="p-3">4,950</td>
<td className="p-3">269,000</td>
<td className="p-3">2,684,000</td>
</tr>
</tbody>
</table>
<p className="text-xs text-muted-foreground mt-2">* Stablecoin pair optimization</p>
</div>
<p className="text-muted-foreground leading-relaxed">
Liquidity Party aggregates scarce, low market cap assets into a single pool, providing one-hop liquidity
for exotic pairs without fragmenting LP assets. Constant Product pools would need 190x the LP assets to provide the
same pairwise liquidity as a single 20-asset Liquidity Party pool, due to asset fragmentation.
</p>
</Card>
{/* Lower Fees & Minimized IL */}
<Card className="p-8 space-y-6">
<div className="space-y-4">
<h2 className="text-2xl font-semibold">Lower Fees</h2>
<p className="text-muted-foreground leading-relaxed">
Since market makers offer the option to take either side of the market, they must receive a subsidy
or charge a fee (spread) to compensate for adverse selection (impermanent loss). By protecting LPs
against common value-extraction scenarios, LMSR pools have a reduced risk premium resulting in lower
fees for takers.
</p>
</div>
<div className="space-y-4 border-t pt-6">
<h2 className="text-2xl font-semibold">Minimized Impermanent Loss</h2>
<h3 className="text-xl font-semibold text-primary">Adverse Selection Protection</h3>
<p className="text-muted-foreground leading-relaxed">
All AMM's suffer from Impermanent Loss (IL), also known as adverse selection or toxic order flow. Liquidity
Party uses game theory to minimize IL for LPs, by charging lower fees to small legitimate traders, and
higher fees to large adversarial traders during market dislocation. This means a higher effective rate
for LP's and cheaper swaps for legitimate small traders.
</p>
<p className="text-muted-foreground leading-relaxed">
Liquidity Party swaps guarantee a bounded maximum loss of <code>κ*S*ln(N)</code> where <code>κ</code> is
the pool's liquidity parameter, <code>S</code> is the total size of the pool, and <code>N</code> is the
number of assets in the pool.
</p>
<h3 className="text-xl font-semibold text-primary">No Intra-Pool Arbitrage</h3>
<p className="text-muted-foreground leading-relaxed">
Other multi-asset systems can provide inconsistent price quotes, allowing arbitragers to
extract value from LP's by <i>trading assets inside the same pool against each other.</i> With Liquidity
Party, no intra-pool arbitrage is possible, because the mathematics guarantee fully consistent price
quotes on all pairs in the pool.
</p>
</div>
</Card>
{/* Open Source Development */}
<Card className="p-8 space-y-4">
<h2 className="text-2xl font-semibold">Transparent Open Source</h2>
<p className="text-muted-foreground leading-relaxed">
The Liquidity Party contract source code fully transparent to the public and verified by Etherscan.
</p>
<p className="text-muted-foreground leading-relaxed">
View our contracts on{' '}
<a
href="https://github.com/Liquidity-Party/lmsr-amm"
target="liqp_github"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
GitHub
</a>
</p>
<p className="text-muted-foreground leading-relaxed">
Verify our contracts on{' '}
<a
href="https://etherscan.io/address/0x42977f565971F6D288a05ddEbC87A17276F71A29#code"
target="liqp_etherscan"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Etherscan (Sepolia)
</a>
</p>
</Card>
{/* Footer */}
<div className="text-center py-8 border-t">
<p className="text-sm text-muted-foreground">
Liquidity Party is offered by Dexorder Trading Services, Ltd. (British Virgin Islands)
</p>
</div>
</div>
);
}

View File

@@ -1,10 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1209)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(9.25540316264299,0,0,9.25540316264299,100,100)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="86.435997" height="86.435997"><svg version="1.1" viewBox="0 0 86.435997 86.435997" id="svg188" sodipodi:docname="logo-flower.svg" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1209)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(9.25540316264299,0,0,9.25540316264299,100,100)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="86.435997" height="86.435997"><svg version="1.1" viewBox="0 0 86.435997 86.435997" id="svg188" sodipodi:docname="logo-splash.svg" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs192"><clipPath id="SvgjsClipPath1209"><rect width="1000" height="1000" x="0" y="0" rx="500" ry="500"></rect></clipPath></defs>
<sodipodi:namedview id="namedview190" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="5.6893399" inkscape:cx="83.489475" inkscape:cy="69.515973" inkscape:window-width="1864" inkscape:window-height="1131" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg188"></sodipodi:namedview>
<!-- Flower -->
<g fill="#000000" id="g186" transform="translate(-1.782,-1.782)">
<g id="g184">
<circle fill="#000000" cx="45" cy="44.999001" r="5.277" id="circle148"></circle>
<g id="g182">
<g id="g152">
<path fill="#0d47a1" class="blue1" d="m 45.214,33.672 c 24.117,-31.271 0.369,-31.89 0.369,-31.89 0,0 -23.756,0.069 -0.369,31.89 z" id="path150"></path>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,6 +1,34 @@
import '@rainbow-me/rainbowkit/styles.css';
import '@/app/globals.css';
import { Providers } from '@/components/providers';
import { Metadata } from 'next';
import Script from "next/script";
export const metadata: Metadata = {
metadataBase: new URL('https://liquidity.party'),
title: 'Liquidity Party',
description: 'Game-Theoretic Multi-asset DEX',
openGraph: {
url: 'https://liquidity.party/',
type: 'website',
title: 'Liquidity Party',
description: 'Game-Theoretic Multi-asset DEX',
images: [
{
url: '/social-card-light.png',
width: 1200,
height: 630,
alt: 'Liquidity Party',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'Liquidity Party',
description: 'Game-Theoretic Multi-asset DEX',
images: ['/social-card-light.png'],
},
};
export default function RootLayout({
children,
@@ -9,11 +37,20 @@ export default function RootLayout({
}) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<title>Liquidity Party</title>
<meta name="description" content="Decentralized Exchange for Multi-Asset AMM Pools" />
</head>
<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>
</body>
</html>

View File

@@ -1,6 +0,0 @@
{
"31337": {
"IPartyPlanner": "0xB35D3C9b9f2Fd72FAAb282E8Dd56da31FAA30E3d",
"IPartyPoolViewer": "0x238213078DbD09f2D15F4c14c02300FA1b2A81BB"
}
}

View File

@@ -1,31 +1,11 @@
'use client';
import { useTranslation } from 'react-i18next';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { SwapForm } from '@/components/swap-form';
export default function HomePage() {
const { t } = useTranslation();
return (
<div className="max-w-2xl mx-auto">
<Tabs defaultValue="swap" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-8">
<TabsTrigger value="swap">{t('nav.swap')}</TabsTrigger>
<TabsTrigger value="stake">{t('nav.stake')}</TabsTrigger>
</TabsList>
<TabsContent value="swap">
<SwapForm />
</TabsContent>
<TabsContent value="stake">
<div className="text-center py-12 text-muted-foreground">
<h2 className="text-2xl font-semibold mb-2">{t('stake.title')}</h2>
<p>{t('stake.comingSoon')}</p>
</div>
</TabsContent>
</Tabs>
<div className="w-full max-w-md mx-auto">
<SwapForm />
</div>
);
}

11
src/app/stake/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
'use client';
import { StakeForm } from '@/components/stake-form';
export default function StakePage() {
return (
<div className="w-full max-w-md mx-auto">
<StakeForm />
</div>
);
}

5
src/app/terms/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import TosCard from '@/components/tos-card';
export default function TermsPage() {
return <TosCard />;
}

11
src/app/unstake/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
'use client';
import { StakeForm } from '@/components/stake-form';
export default function UnstakePage() {
return (
<div className="w-full max-w-md mx-auto">
<StakeForm defaultMode="unstake" />
</div>
);
}

View File

@@ -4,18 +4,27 @@ import { useTheme } from 'next-themes';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { LanguageSelector } from '@/components/language-selector';
import { Moon, Sun } from 'lucide-react';
import { Moon, Sun, Menu, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export function Header() {
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const [mounted, setMounted] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
// Close mobile menu when route changes
setMobileMenuOpen(false);
}, [pathname]);
const toggleTheme = () => {
console.log('Toggle clicked, current theme:', theme);
const newTheme = theme === 'dark' ? 'light' : 'dark';
@@ -24,33 +33,118 @@ export function Header() {
};
const logoSrc = !mounted ? '/logo-dark.svg' : theme === 'dark' ? '/logo-dark.svg' : '/logo-light.svg';
const mobileLogoSrc = '/logo-splash.svg';
const navLinks = [
{ href: '/', label: 'Swap' },
{ href: '/stake', label: 'Stake' },
{ href: '/unstake', label: 'Unstake' },
{ href: '/about', label: 'About' },
];
return (
<header className="border-b">
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<img
src={logoSrc}
alt="Liquidity Party"
className="h-8 w-auto"
/>
<div className="flex items-center gap-3">
{/* Desktop logo - just a link */}
<Link href="/" className="hidden sm:block">
<img
src={logoSrc}
alt="Liquidity Party"
className="h-8 w-auto cursor-pointer"
/>
</Link>
{/* Mobile logo - toggles menu */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="sm:hidden focus:outline-none"
>
<img
src={mobileLogoSrc}
alt="Liquidity Party"
className="h-8 w-8 cursor-pointer"
/>
<span className="sr-only">Toggle menu</span>
</button>
<span className="bg-yellow-500/20 text-yellow-500 text-xs font-bold px-2 py-1 rounded border border-yellow-500/50">
BETA
</span>
</div>
<div className="flex items-center gap-2">
{/* Language Selector */}
<LanguageSelector />
{/* Desktop Navigation Links */}
<nav className="hidden md:flex items-center gap-1 mr-2">
{navLinks.map((link) => (
<Link key={link.href} href={link.href}>
<Button
variant="ghost"
size="sm"
className={`text-sm font-medium ${pathname === link.href ? 'bg-accent' : ''}`}
>
{link.label}
</Button>
</Link>
))}
</nav>
{/* Theme Toggle */}
<Button variant="ghost" size="icon" onClick={toggleTheme} type="button">
{/* Language Selector - Hidden on small screens */}
<div className="hidden sm:block">
<LanguageSelector />
</div>
{/* Theme Toggle - Hidden on mobile */}
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
type="button"
className="hidden sm:flex"
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
{/* Wallet Connect */}
{/* Wallet Connect - Always visible */}
<ConnectButton />
</div>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden border-t">
<nav className="container mx-auto px-4 py-4 flex flex-col gap-2">
{navLinks.map((link) => (
<Link key={link.href} href={link.href}>
<Button
variant="ghost"
className={`w-full justify-start text-base font-medium ${pathname === link.href ? 'bg-accent' : ''}`}
>
{link.label}
</Button>
</Link>
))}
<div className="flex items-center gap-2 pt-2 border-t mt-2">
<div className="flex-1">
<LanguageSelector />
</div>
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
type="button"
className="flex-shrink-0"
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</div>
</nav>
</div>
)}
</header>
);
}

View File

@@ -2,7 +2,7 @@
import { getDefaultConfig, RainbowKitProvider, darkTheme, lightTheme } from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import { mainnet, polygon, optimism, arbitrum, base } from 'wagmi/chains';
import { mainnet, polygon, optimism, arbitrum, base, sepolia } from 'wagmi/chains';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { ThemeProvider, useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
@@ -10,29 +10,37 @@ import { defineChain } from 'viem';
import { TranslationsProvider } from '@/providers/translations-provider';
import { Header } from '@/components/header';
import { ToastProvider } from '@/components/ui/toast';
import deployments from '@/contracts/liqp-deployments.json';
const mockchain = defineChain({
id: 31337,
name: 'Mockchain',
nativeCurrency: {
decimals: 18,
name: 'Ether',
symbol: 'ETH',
name: 'Test Ether',
symbol: 'TETH',
},
rpcUrls: {
default: { http: ['http://localhost:8545'] },
},
blockExplorers: {
default: { name: 'Explorer', url: 'http://localhost:8545' },
},
testnet: true,
});
const allChains = [mainnet, polygon, optimism, arbitrum, base, sepolia, mockchain];
const chains = allChains.filter(chain =>
Object.keys(deployments || {}).includes(chain.id.toString())
);
if (chains.length === 0) {
console.error('No chains found with deployed contracts.');
}
const config = getDefaultConfig({
appName: 'Liquidity Party',
projectId: 'YOUR_PROJECT_ID', // Get this from https://cloud.walletconnect.com
chains: [mainnet, polygon, optimism, arbitrum, base, mockchain],
ssr: false,
appName: 'Liquidity Party',
projectId: 'YOUR_PROJECT_ID', // Get this from https://cloud.walletconnect.com
chains: chains as any,
ssr: false,
});
const queryClient = new QueryClient({
@@ -87,12 +95,24 @@ export function Providers({ children }: { children: React.ReactNode }) {
storageKey="liquidity-party-theme"
>
<Web3Provider>
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 container mx-auto px-4 py-8">
{children}
</main>
</div>
<ToastProvider>
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 container mx-auto px-4 py-8">
{children}
</main>
<footer className="py-4 text-center text-sm text-muted-foreground border-t">
<div className="mb-2">
©{new Date().getFullYear()} Dexorder Trading Services, Ltd. (BVI)
</div>
<div>
<a href="/terms" className="hover:text-foreground underline transition-colors">
Terms of Service
</a>
</div>
</footer>
</div>
</ToastProvider>
</Web3Provider>
</ThemeProvider>
</TranslationsProvider>

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,105 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { ArrowDownUp, ChevronDown } from 'lucide-react';
import { useAccount } from 'wagmi';
import { useTokenDetails, type TokenDetails } from '@/hooks/usePartyPlanner';
import { formatUnits } from 'viem';
import { ArrowDownUp, ChevronDown, Settings, CheckCircle, XCircle, Loader2 } from 'lucide-react';
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);
const [selectedToToken, setSelectedToToken] = useState<TokenDetails | null>(null);
const [isFromDropdownOpen, setIsFromDropdownOpen] = useState(false);
const [isToDropdownOpen, setIsToDropdownOpen] = useState(false);
const [maxSlippage, setMaxSlippage] = useState<string>('1'); // Default 1%
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false);
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
const [transactionError, setTransactionError] = useState<string | null>(null);
const [actualSwapAmounts, setActualSwapAmounts] = useState<ActualSwapAmounts | null>(null);
const fromDropdownRef = useRef<HTMLDivElement>(null);
const toDropdownRef = useRef<HTMLDivElement>(null);
// Use the custom hook to get all token details with a single batched RPC call
const { tokenDetails, loading, error } = useTokenDetails(address);
const { tokenDetails, loading } = useTokenDetails(address);
// Trigger the hook to fetch data when address is available
useEffect(() => {
if (tokenDetails) {
console.log('Token details loaded in swap-form');
// Get available tokens for the selected "from" token
const { availableTokens, error: poolsError } = useGetPoolsByToken(selectedFromToken?.address);
// Only calculate swap amounts when both tokens are selected
// Use useMemo to prevent creating a new array reference on every render
const filteredAvailableTokens = useMemo(() => {
if (selectedFromToken && selectedToToken && availableTokens) {
return availableTokens.filter(token =>
token.address.toLowerCase() === selectedToToken.address.toLowerCase()
);
}
}, [tokenDetails]);
return null;
}, [selectedFromToken, selectedToToken, availableTokens]);
// Get the current slippage value
const currentSlippage = parseFloat(maxSlippage) || 1;
// Calculate swap amounts for the selected token pair only
const { swapAmounts } = useSwapAmounts(
filteredAvailableTokens,
fromAmount,
selectedFromToken?.decimals || 18,
currentSlippage
);
// Check if user has insufficient balance
const hasInsufficientBalance = useMemo(() => {
if (!selectedFromToken || !fromAmount || fromAmount === '') {
return false;
}
try {
const inputAmount = parseUnits(fromAmount, selectedFromToken.decimals);
return inputAmount > selectedFromToken.balance;
} catch {
return false;
}
}, [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
const { executeSwap, estimateGas, isSwapping, gasEstimate, isEstimatingGas } = useSwap();
// Update "You Receive" amount when swap calculation completes
useEffect(() => {
if (hasInsufficientBalance) {
setToAmount('');
return;
}
if (swapAmounts && swapAmounts.length > 0 && selectedToToken) {
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]);
// Close dropdowns when clicking outside
useEffect(() => {
@@ -47,30 +116,127 @@ export function SwapForm() {
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSwap = () => {
// Swap logic will be implemented later
console.log('Swap clicked');
const handleSwap = async () => {
if (!swapAmounts || swapAmounts.length === 0) {
console.error('No swap amounts available');
return;
}
if (!selectedFromToken || !selectedToToken) {
console.error('Tokens not selected');
return;
}
setTransactionStatus('pending');
setTransactionError(null);
try {
// Use the shared helper to select the best swap route
const bestRoute = selectBestSwapRoute(swapAmounts);
if (!bestRoute) {
console.error('No valid swap route found');
setTransactionError('No valid swap route found');
setTransactionStatus('error');
return;
}
// Use the user's input directly as maxAmountIn
const maxAmountIn = parseUnits(fromAmount, selectedFromToken.decimals);
// Execute the swap and capture actual amounts from the transaction
const result = await executeSwap(
bestRoute.poolAddress,
selectedFromToken.address,
bestRoute.inputTokenIndex,
bestRoute.outputTokenIndex,
maxAmountIn,
currentSlippage
);
// Store the actual swap amounts if available
if (result?.actualSwapAmounts) {
setActualSwapAmounts(result.actualSwapAmounts);
}
setTransactionStatus('success');
} catch (err) {
console.error('Swap failed:', err);
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
setTransactionStatus('error');
}
};
const handleCloseModal = () => {
if (transactionStatus === 'success') {
// Clear the form after successful swap
setFromAmount('');
setToAmount('');
setSelectedFromToken(null);
setSelectedToToken(null);
}
setTransactionStatus('idle');
setTransactionError(null);
};
const switchTokens = () => {
// Switch tokens logic
// Switch both tokens and amounts
const tempFromAmount = fromAmount;
const tempFromToken = selectedFromToken;
setFromAmount(toAmount);
setToAmount(fromAmount);
setToAmount(tempFromAmount);
setSelectedFromToken(selectedToToken);
setSelectedToToken(tempFromToken);
};
// Estimate gas when swap parameters change
useEffect(() => {
const estimateSwapGas = async () => {
if (!swapAmounts || swapAmounts.length === 0 || !selectedFromToken || !selectedToToken || !fromAmount || !isConnected) {
return;
}
await estimateGas();
};
estimateSwapGas();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [swapAmounts, selectedFromToken, selectedToToken, fromAmount, currentSlippage, isConnected]);
return (
<Card className="w-full max-w-md mx-auto">
<Card className="w-full max-w-md mx-auto relative">
<CardHeader>
<CardTitle>{t('swap.title')}</CardTitle>
<div className="flex justify-between items-center">
<CardTitle>Swap</CardTitle>
<Button
variant="ghost"
size="icon"
onClick={() => setIsSettingsOpen(true)}
className="h-8 w-8"
>
<Settings className="h-5 w-5" />
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* From Token */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<div className="flex justify-between items-center text-sm">
<label className="text-muted-foreground">{t('swap.youPay')}</label>
<span className="text-muted-foreground">
{t('swap.balance')}: {selectedFromToken ? formatUnits(selectedFromToken.balance, selectedFromToken.decimals) : '0.00'}
</span>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => {
if (selectedFromToken) {
setFromAmount(formatUnits(selectedFromToken.balance, selectedFromToken.decimals));
}
}}
disabled={!selectedFromToken}
>
MAX
</Button>
</div>
<div className="flex gap-2">
<Input
@@ -80,7 +246,7 @@ export function SwapForm() {
onChange={(e) => setFromAmount(e.target.value)}
className="text-2xl h-16"
/>
<div className="relative min-w-[160px]" ref={fromDropdownRef}>
<div className="relative min-w-[160px] space-y-1" ref={fromDropdownRef}>
<Button
variant="secondary"
className="w-full h-16 justify-between"
@@ -93,22 +259,22 @@ export function SwapForm() {
)}
<ChevronDown className="h-4 w-4 ml-2" />
</Button>
<div className="text-xs text-muted-foreground text-right">
{t('swap.balance')}: {selectedFromToken ? formatUnits(selectedFromToken.balance, selectedFromToken.decimals) : '0.00'}
</div>
{isFromDropdownOpen && (
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
{tokenDetails && tokenDetails.length > 0 ? (
tokenDetails.map((token) => (
<button
key={token.address}
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none flex items-center justify-between"
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
onClick={() => {
setSelectedFromToken(token);
setIsFromDropdownOpen(false);
}}
>
<span className="font-medium">{token.symbol}</span>
<span className="text-sm text-muted-foreground">
{formatUnits(token.balance, token.decimals)}
</span>
</button>
))
) : (
@@ -136,11 +302,8 @@ export function SwapForm() {
{/* To Token */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<div className="text-sm">
<label className="text-muted-foreground">{t('swap.youReceive')}</label>
<span className="text-muted-foreground">
{t('swap.balance')}: {selectedToToken ? formatUnits(selectedToToken.balance, selectedToToken.decimals) : '0.00'}
</span>
</div>
<div className="flex gap-2">
<Input
@@ -149,12 +312,15 @@ export function SwapForm() {
value={toAmount}
onChange={(e) => setToAmount(e.target.value)}
className="text-2xl h-16"
disabled={!selectedFromToken}
readOnly
/>
<div className="relative min-w-[160px]" ref={toDropdownRef}>
<div className="relative min-w-[160px] space-y-1" ref={toDropdownRef}>
<Button
variant="secondary"
className="w-full h-16 justify-between"
onClick={() => setIsToDropdownOpen(!isToDropdownOpen)}
disabled={!selectedFromToken}
>
{selectedToToken ? (
<span className="font-medium">{selectedToToken.symbol}</span>
@@ -163,27 +329,38 @@ export function SwapForm() {
)}
<ChevronDown className="h-4 w-4 ml-2" />
</Button>
<div className="text-xs text-muted-foreground text-right">
{t('swap.balance')}: {selectedToToken ? formatUnits(selectedToToken.balance, selectedToToken.decimals) : '0.00'}
</div>
{isToDropdownOpen && (
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
{tokenDetails && tokenDetails.length > 0 ? (
tokenDetails.map((token) => (
<button
key={token.address}
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none flex items-center justify-between"
onClick={() => {
setSelectedToToken(token);
setIsToDropdownOpen(false);
}}
>
<span className="font-medium">{token.symbol}</span>
<span className="text-sm text-muted-foreground">
{formatUnits(token.balance, token.decimals)}
</span>
</button>
))
{availableTokens && availableTokens.length > 0 && tokenDetails ? (
// Filter tokenDetails to only show tokens in availableTokens
tokenDetails
.filter((token) =>
availableTokens.some((availToken) =>
availToken.address.toLowerCase() === token.address.toLowerCase()
)
)
.map((token) => (
<button
key={token.address}
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
onClick={() => {
setSelectedToToken(token);
setIsToDropdownOpen(false);
}}
>
<span className="font-medium">{token.symbol}</span>
</button>
))
) : selectedFromToken ? (
<div className="px-4 py-3 text-sm text-muted-foreground">
{loading ? 'Loading available tokens...' : poolsError || 'No tokens available for swap'}
</div>
) : (
<div className="px-4 py-3 text-sm text-muted-foreground">
{loading ? 'Loading tokens...' : 'No tokens available'}
Select a token in "You Pay" first
</div>
)}
</div>
@@ -192,15 +369,254 @@ export function SwapForm() {
</div>
</div>
{/* Swap Button */}
{/* Error message for unsupported tokens */}
{poolsError && selectedFromToken && (
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive">{poolsError}</p>
</div>
)}
{/* Error message for insufficient balance */}
{hasInsufficientBalance && (
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive">Insufficient balance</p>
</div>
)}
{/* Error message for slippage exceeding 5% */}
{slippageExceedsLimit && (
<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 swap is {Math.abs(swapAmounts![0].calculatedSlippage!).toFixed(2)}%.
We cannot process this swap as you may lose too much money due to the high slippage.
</p>
</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 */}
{isConnected && fromAmount && toAmount && (
<div className="px-4 py-2 bg-muted/30 rounded-lg space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Estimated Gas Cost:</span>
<span className="font-medium">
{isEstimatingGas ? 'Calculating...' : gasEstimate ? (gasEstimate.estimatedCostUsd.startsWith('<') ? gasEstimate.estimatedCostUsd : `$${gasEstimate.estimatedCostUsd}`) : '-'}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Max Slippage:</span>
<span className="font-medium">{maxSlippage}%</span>
</div>
{swapAmounts && swapAmounts.length > 0 && selectedFromToken && fromAmount && (
<>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Total Amount In:</span>
<span className="font-medium">
{formatUnits(swapAmounts[0].amountIn, selectedFromToken.decimals)} {selectedFromToken.symbol}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Fee:</span>
<span className="font-medium">
{formatUnits(swapAmounts[0].fee, selectedFromToken.decimals)} {selectedFromToken.symbol}
{' '}
({((Number(swapAmounts[0].fee) / Number(swapAmounts[0].amountIn)) * 100).toFixed(2)}%)
</span>
</div>
</>
)}
</div>
)}
{/* Review Button */}
<Button
className="w-full h-14 text-lg"
onClick={handleSwap}
disabled={!isConnected || !fromAmount || !toAmount}
onClick={() => setIsReviewModalOpen(true)}
disabled={!isConnected || !fromAmount || !toAmount || !!poolsError || hasInsufficientBalance || slippageExceedsLimit}
>
{!isConnected ? t('swap.connectWalletToSwap') : t('swap.swapButton')}
{!isConnected
? t('swap.connectWalletToSwap')
: hasInsufficientBalance
? 'Insufficient Balance'
: slippageExceedsLimit
? 'Slippage Too High'
: 'Review'}
</Button>
</CardContent>
{/* Settings Modal */}
{isSettingsOpen && (
<>
<div
className="fixed inset-0 z-50 bg-black/80 animate-in fade-in-0"
onClick={() => setIsSettingsOpen(false)}
/>
<div className="fixed left-[50%] top-[50%] z-50 w-full max-w-sm translate-x-[-50%] translate-y-[-50%] animate-in fade-in-0 zoom-in-95">
<div className="bg-background border rounded-lg shadow-lg p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Settings</h2>
<button
onClick={() => setIsSettingsOpen(false)}
className="rounded-sm opacity-70 hover:opacity-100 transition-opacity"
>
<ChevronDown className="h-4 w-4" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="text-sm font-medium block mb-2">
Max Slippage
</label>
<div className="relative">
<Input
type="number"
value={maxSlippage}
onChange={(e) => setMaxSlippage(e.target.value)}
className="pr-8"
step="0.1"
min="0"
max="100"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
%
</span>
</div>
<p className="text-xs text-muted-foreground mt-2">
You will be warned if the slippage exceeds this setting, and you can choose whether to proceed with the trade.
</p>
</div>
</div>
</div>
</div>
</>
)}
{/* Review Modal */}
<SwapReviewModal
open={isReviewModalOpen}
onOpenChange={setIsReviewModalOpen}
fromToken={selectedFromToken}
toToken={selectedToToken}
fromAmount={fromAmount}
toAmount={toAmount}
slippage={currentSlippage}
gasEstimate={gasEstimate}
fee={swapAmounts && swapAmounts.length > 0 ? swapAmounts[0].fee : null}
onConfirm={async () => {
await handleSwap();
setIsReviewModalOpen(false);
}}
isSwapping={isSwapping}
/>
{/* Transaction Modal Overlay */}
{transactionStatus !== 'idle' && (
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
<div className="bg-card border rounded-lg p-8 max-w-sm w-full mx-4 shadow-lg">
{transactionStatus === 'pending' && (
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-16 w-16 animate-spin text-primary" />
<h3 className="text-xl font-semibold text-center">Approving Swap</h3>
<p className="text-sm text-muted-foreground text-center">
Swapping {fromAmount} {selectedFromToken?.symbol} {toAmount} {selectedToToken?.symbol}
</p>
<p className="text-xs text-muted-foreground text-center">
Please confirm the transactions in your wallet
</p>
</div>
)}
{transactionStatus === 'success' && (
<div className="flex flex-col items-center space-y-4">
<CheckCircle className="h-16 w-16 text-green-500" />
<h3 className="text-xl font-semibold text-center">Swap Confirmed!</h3>
{/* Display actual amounts or estimates */}
<div className="w-full space-y-3">
{actualSwapAmounts && selectedFromToken && selectedToToken ? (
// Show actual amounts from transaction
<>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Swapped:</span>
<span className="font-medium">{formatUnits(actualSwapAmounts.amountIn, selectedFromToken.decimals)} {selectedFromToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Received:</span>
<span className="font-medium">{formatUnits(actualSwapAmounts.amountOut, selectedToToken.decimals)} {selectedToToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Fee:</span>
<span className="font-medium">{formatUnits(actualSwapAmounts.lpFee + actualSwapAmounts.protocolFee, selectedFromToken.decimals)} {selectedFromToken.symbol}</span>
</div>
</>
) : (
// Fallback to estimates
<>
<p className="text-sm text-muted-foreground text-center">
Successfully swapped {fromAmount} {selectedFromToken?.symbol} to {toAmount} {selectedToToken?.symbol}
<br />
<span className="text-xs italic opacity-70">
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.
</span>
</p>
</>
)}
</div>
<Button
onClick={handleCloseModal}
className="w-full mt-4"
>
Close
</Button>
</div>
)}
{transactionStatus === 'error' && (
<div className="flex flex-col items-center space-y-4">
<XCircle className="h-16 w-16 text-destructive" />
<h3 className="text-xl font-semibold text-center">Swap Failed</h3>
<p className="text-sm text-muted-foreground text-center break-words">
{transactionError || 'Transaction failed'}
</p>
<Button
onClick={handleCloseModal}
variant="outline"
className="w-full mt-4"
>
Close
</Button>
</div>
)}
</div>
</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,140 @@
'use client';
import { Button } from '@/components/ui/button';
import { ArrowDown, X } from 'lucide-react';
import type { TokenDetails } from '@/hooks/usePartyPlanner';
import type { GasEstimate } from '@/hooks/usePartyPool';
import { useToast } from '@/components/ui/toast';
interface SwapReviewModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
fromToken: TokenDetails | null;
toToken: TokenDetails | null;
fromAmount: string;
toAmount: string;
slippage: number;
gasEstimate: GasEstimate | null;
fee: bigint | null;
onConfirm: () => void;
isSwapping: boolean;
}
export function SwapReviewModal({
open,
onOpenChange,
fromToken,
toToken,
fromAmount,
toAmount,
slippage,
gasEstimate,
fee,
onConfirm,
isSwapping,
}: SwapReviewModalProps) {
if (!open) return null;
// Calculate exchange rate
const exchangeRate = fromAmount && toAmount && parseFloat(fromAmount) > 0
? (parseFloat(toAmount) / parseFloat(fromAmount)).toFixed(6)
: '0';
// Format fee
const feeFormatted = fee && toToken
? (Number(fee) / Math.pow(10, toToken.decimals)).toFixed(6)
: '0';
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-50 bg-black/80 animate-in fade-in-0"
onClick={() => onOpenChange(false)}
/>
{/* Modal */}
<div className="fixed left-[50%] top-[50%] z-50 w-full max-w-md translate-x-[-50%] translate-y-[-50%] animate-in fade-in-0 zoom-in-95 slide-in-from-left-1/2 slide-in-from-top-[48%]">
<div className="bg-background border rounded-lg shadow-lg p-6">
{/* Header */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Review Swap</h2>
<button
onClick={() => onOpenChange(false)}
className="rounded-sm opacity-70 hover:opacity-100 transition-opacity"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="space-y-4">
{/* From Token */}
<div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<div>
<div className="text-sm text-muted-foreground">You pay</div>
<div className="text-2xl font-semibold">{fromAmount}</div>
</div>
<div className="text-xl font-medium">{fromToken?.symbol}</div>
</div>
{/* Arrow */}
<div className="flex justify-center">
<ArrowDown className="h-5 w-5 text-muted-foreground" />
</div>
{/* To Token */}
<div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<div>
<div className="text-sm text-muted-foreground">You receive</div>
<div className="text-2xl font-semibold">{toAmount}</div>
</div>
<div className="text-xl font-medium">{toToken?.symbol}</div>
</div>
{/* Details */}
<div className="space-y-2 pt-2">
{/* Exchange Rate */}
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Rate</span>
<span className="font-medium">
1 {fromToken?.symbol} = {exchangeRate} {toToken?.symbol}
</span>
</div>
{/* Fee */}
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Fee</span>
<span className="font-medium">{feeFormatted} {toToken?.symbol}</span>
</div>
{/* Slippage */}
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Slippage Tolerance</span>
<span className="font-medium">{slippage}%</span>
</div>
{/* Network Fee */}
{gasEstimate && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Network Fee</span>
<span className="font-medium">
{gasEstimate.estimatedCostUsd.startsWith('<') ? gasEstimate.estimatedCostUsd : `$${gasEstimate.estimatedCostUsd}`}
</span>
</div>
)}
</div>
{/* Approve and Swap Button */}
<Button
className="w-full h-12 text-lg"
onClick={onConfirm}
disabled={isSwapping}
>
{isSwapping ? 'Swapping...' : 'Approve and Swap'}
</Button>
</div>
</div>
</div>
</>
);
}

675
src/components/tos-card.tsx Normal file
View File

@@ -0,0 +1,675 @@
import React from 'react';
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">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>")
carefully because they govern your use of the website (and all subdomains and subpages
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>," "
<b>we</b>," "<b>our</b>," or "<b>us</b>").
</div>
<div className="mb-4 leading-relaxed">
<b>
BY USING THE DEXORDER SERVICE, YOU REPRESENT THAT (I) YOU ARE NOT LOCATED WITHIN THE
UNITED STATES; AND (II) YOU ARE NOT A PERSON OR ENTITY WHO IS RESIDENT IN, A CITIZEN OF,
IS LOCATED IN, IS INCORPORATED IN, OR HAS A REGISTERED OFFICE IN ANY RESTRICTED
TERRITORY, AS DEFINED BELOW (ANY SUCH PERSON OR ENTITY FROM WITHIN THE UNITED STATES OR
A RESTRICTED TERRITORY, IS REFERRED TO HEREIN AS A "RESTRICTED PERSON").
</b>
</div>
<div className="mb-4 leading-relaxed">
WHEN YOU AGREE TO THESE TERMS, YOU ARE AGREEING (WITH LIMITED EXCEPTION) TO RESOLVE ANY
DISPUTE BETWEEN YOU AND DEXORDER THROUGH BINDING, INDIVIDUAL ARBITRATION RATHER THAN IN
COURT. PLEASE REVIEW CAREFULLY SECTION 16 (DISPUTE RESOLUTION) BELOW FOR DETAILS REGARDING
ARBITRATION. HOWEVER, IF YOU ARE A RESIDENT OF A JURISDICTION WHERE APPLICABLE LAW
PROHIBITS ARBITRATION OF DISPUTES, THE AGREEMENT TO ARBITRATE IN SECTION 16 WILL NOT APPLY
TO YOU, BUT THE PROVISIONS OF SECTION 15 (GOVERNING LAW) WILL APPLY INSTEAD.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">1. Description of Dexorder Service</h2>
<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>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.
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
time, including blog posts, data, articles, links to third-party content, Discord content,
news feeds, tutorials, tweets, and videos. Before you make any financial, legal, technical,
or other decisions involving the Dexorder Service, you should seek independent professional
advice from a licensed and qualified individual in the area for which such advice would be
appropriate. Because the Dexorder Service provides information about the Protocol, these
Terms also provide some information about the use of the Protocol. This information is not
intended to be comprehensive or address all aspects of the Protocol.
</div>
<div className="mb-4 leading-relaxed">
(c) <b>Our Relationship</b>. You acknowledge and agree that Dexorder is an online platform
provider and not a financial institution, broker dealer, exchange or money services
business. Dexorder does not direct or control the day-to-day activities of users accessing
the Dexorder Service. Neither we nor any affiliated entity is a party to any transaction on
the blockchain network underlying the Protocol; we do not have possession, custody or
control over any cryptoassets appearing on the Dexorder Service or on the Protocol; and we
do not have possession, custody, or control over any user's funds or cryptoassets. Further,
we do not store, send, or receive any funds or cryptoassets on your behalf. You understand
that when you interact with any Protocol smart contracts, you retain control over your
cryptoassets at all times. You are solely responsible for evaluating any proposed technical
changes and how such changes may alter current or future Interactions. Furthermore, you
understand and acknowledge that only you have absolute and ultimate authority over the
implementation of any such changes and the responsibility therefor. The private key
associated with your Vault is the only private key that can control the cryptoassets in the
Vault. You alone are responsible for securing your private keys. We do not have access to
your private keys. Because the Protocol is non-custodial, we are not intermediaries, agents,
advisors, or custodians, and we do not have a fiduciary relationship or obligation to you
regarding any other decisions or activities that you affect when using the Dexorder Service
or interacting with the Protocol. You acknowledge that we, for the avoidance of doubt, do
not have any information regarding any users, users' identities, or services beyond what is
available, obtainable publicly via the blockchain, or shared by you when you access the
Dexorder Service. We are not responsible for any activities you engage in when using the
Dexorder Service, and you should understand the risks associated with cryptoassets,
blockchain technology generally, and the Interface.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">2. Agreement to Terms</h2>
<div className="mb-4 leading-relaxed">
By using our Dexorder Service, you agree to be bound by these Terms. If you don't agree to
be bound by these Terms, do not use the Dexorder Service.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">3. Changes to these Terms or the Dexorder Service</h2>
<div className="mb-4 leading-relaxed">
We may update the Terms, including any addendum terms, from time to time in our sole
discretion. If we do, we'll let you know by posting the updated Terms on the Site and/or may
also send other communications. It's important that you review the Terms whenever we update
them or you use the Dexorder Service. If you continue to use the Dexorder Service after we
have posted updated Terms it means that you accept and agree to the changes. If you don't
agree to be bound by the changes, you may not use the Dexorder Service anymore. Because our
Dexorder Service is evolving over time we may change or discontinue all or any part of the
Dexorder Service, at any time and without notice, at our sole discretion.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">4. Who May Use the Dexorder Service?</h2>
<div className="mb-4 leading-relaxed">
(a) <u>Eligibility</u>. The Dexorder Service is only available to users in certain
jurisdictions outside of the United States and that are at least 18 years old, capable of
forming a binding contract with the Dexorder and not otherwise barred from using the
Dexorder Service under Applicable Law. You may not attempt to access or use the Dexorder
Service if you are not permitted to do so (including without limitation if you are a
Restricted Person).
</div>
<div className="mb-4 leading-relaxed">
(b) <u>Compliance</u>. You certify that you will comply with all Applicable Law when using
the Dexorder Service. You are solely responsible for ensuring that your access and use of
the Dexorder Service in such country, territory, or jurisdiction does not violate any
Applicable Laws. You must not use any software or networking techniques, including use of a
virtual private network ("<b>VPN</b>") to circumvent or attempt to circumvent this
prohibition. We reserve the right to monitor the locations from which our Dexorder Service
is accessed. Furthermore, we reserve the right, at any time, in our sole discretion, to
block access to the Dexorder Service, in whole or in part, from any geographic location, IP
addresses, and unique device identifiers
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">5. Use of the Dexorder Service</h2>
<div className="mb-4 leading-relaxed">
(a) <u>User Representations and Warranties</u>. As a condition to accessing or using the
Dexorder Service, you represent and warrant to Dexorder that:
</div>
<div className="mb-4 leading-relaxed ml-8">
(i) if you are entering into these Terms as an individual, then you are of legal age in the
jurisdiction in which you reside and you have the legal capacity to enter into these Terms
and be bound by them and if you are entering into these Terms as an entity, then you must
have the legal authority to accept these Terms on that entity's behalf, in which case "you"
(except as used in this paragraph) will mean that entity;
</div>
<div className="mb-4 leading-relaxed ml-8">(ii) you are not in or residing in the United States;</div>
<div className="mb-4 leading-relaxed ml-8">
(iii) you are not in or residing in Cuba, Iran, North Korea, Syria, Belarus, Russia, and the
Crimea, Luhansk, Donetsk, Zaporizhzhia, and Kherson regions of Ukraine, or any other country
or jurisdiction to which the Cayman Islands, the United Kingdom, United States, the United
Nations Security Council, or the European Union embargoes goods or imposes similar sanctions
(collectively, "<b>Restricted Territories</b>");
</div>
<div className="mb-4 leading-relaxed ml-8">
(iv) you are not on any sanctions list or equivalent maintained by the Cayman Islands, the
United Kingdom, United States, the United Nations Security Council, or the European Union
(collectively, "<b>Sanctions Lists Persons</b>") and you do not intend to transact with any
Restricted Person or Sanctions List Person;
</div>
<div className="mb-4 leading-relaxed ml-8">
(v) you do not, and will not, use VPN software or any other privacy or anonymization tools
or techniques to circumvent, or attempt to circumvent, any restrictions that apply to the
Dexorder Service;
</div>
<div className="mb-4 leading-relaxed ml-8">
(vi) you have obtained all required consents from any individual whose personal information
you transfer to us in connection with your use of the Dexorder Service; and
</div>
<div className="mb-4 leading-relaxed ml-8">
(vii) all information that you provide through the Dexorder Service is current, complete,
true, and accurate and you will maintain the security and confidentiality of your private
keys associated with your public wallet address, passwords, API keys, passwords or other
information associated with your Vault or otherwise, as applicable.
</div>
<div className="mb-4 leading-relaxed ml-8">
(viii) your access to the Dexorder Service is not: (a) prohibited by and does not otherwise
violate or assist you to violate any domestic or foreign law, rule, statute, regulation,
by-law, order, protocol, code, decree, or another directive, requirement, or guideline,
published or in force that applies to or is otherwise intended to govern or regulate any
person, property, transaction, activity, event or other matter, including any rule, order,
judgment, directive or other requirement or guideline issued by any domestic or foreign
federal, provincial or state, municipal, local or other governmental, regulatory, judicial or
administrative authority having jurisdiction over Dexorder, you, the Site or the Dexorder
Service, or as otherwise duly enacted, enforceable by law, the common law or equity
(collectively, "<b>Applicable Laws</b>"); or (b) contribute to or facilitate any illegal
activity.
</div>
<div className="mb-4 leading-relaxed">
(b) <u>Limitations</u>. As a condition to accessing or using the Dexorder Service or the
Site, you acknowledge, understand, and agree to the following:
</div>
<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 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)
periodic maintenance procedures or repairs that Dexorder or any of its suppliers or
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.
</div>
<div className="mb-4 leading-relaxed ml-8">
(ii) the Site and the Dexorder Service may evolve, which means Dexorder may apply changes,
replace, or discontinue (temporarily or permanently) the Dexorder Service at any time in its
sole discretion;
</div>
<div className="mb-4 leading-relaxed ml-8">
(iii) Dexorder does not act as an agent for you or any other user of the Site or the
Dexorder Service;
</div>
<div className="mb-4 leading-relaxed ml-8">
(iv) you are solely responsible for your use of the Dexorder Service; and
</div>
<div className="mb-4 leading-relaxed ml-8">
(v) we owe no fiduciary duties or liabilities to you or any other party, and that to the
extent any such duties or liabilities may exist at law or in equity, you hereby irrevocably
disclaim, waive, and eliminate those duties and liabilities.
</div>
<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). 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>
<div className="mb-4 leading-relaxed">
(b) <u>Fees</u>.
</div>
<div className="mb-4 leading-relaxed ml-8">
(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.
</div>
<div className="mb-4 leading-relaxed ml-8">
(ii) There may be associated fees in connection with transactions enacted on a blockchain.
All transactions using blockchains require the payment of gas fees, which are essentially
transaction fees paid on every transaction that occurs on the selected blockchain network.
We do not collect any such fees. Please note that accessing the Protocol may result in you
incurring gas fees, which are non-refundable, and are paid by you in all circumstances. You
pay all gas fees incurred by you as relating to interacting with the Protocol.
</div>
<div className="mb-4 leading-relaxed">
(c) <u>Tax Records and Reporting</u>. You are solely responsible for all costs incurred by
you in using the Dexorder Service, and for determining, collecting, reporting, and paying
all applicable Taxes that you may be required by law to collect and remit to any
governmental or regulatory agencies. As used herein, "<b>Taxes</b>" means the taxes, duties,
levies, tariffs, and other charges imposed by any federal, state, multinational or local
governmental or regulatory authority. We reserve the right to report any activity occurring
using the Dexorder Service to relevant tax authorities as required under Applicable Law. You
are solely responsible for maintaining all relevant Tax records and complying with any
reporting requirements you may have as related to our Dexorder Service. You are further
solely responsible for independently maintaining the accuracy of any record submitted to any
tax authority including any information derived from the Dexorder Service.
</div>
<div className="mb-4 leading-relaxed">
(d) Suspensions or Terminations. In addition to the other suspension and termination rights
in these Terms, we may suspend or terminate your access to the Dexorder Service, or any and
all Interactions, at any time in connection with any Interaction or transaction (i) as
required by Applicable Law or any governmental authority, (ii) if we are unable to process
or execute an Interaction or transaction after several attempts (as described in the
Execution Policy or otherwise in Dexorder's reasonable discretion), or (iii) if we in our
sole and reasonable discretion determine you are violating the terms of any third-party
service provider or these Terms, including, without limitation, if we reasonably believe any
of your representations and warranties may be untrue or inaccurate or you are violating or
have violated any of the geographical restrictions that apply to the Dexorder Service, and
in any case we will not be liable to you for any losses or damages you may suffer as a
result of or in connection with the Dexorder Service being inaccessible to you at any time
or for any reason. Such suspension or termination shall not constitute a breach of these
Terms by Dexorder. In accordance with its anti- money laundering, anti-terrorism,
anti-fraud, and other compliance policies and practices, we may impose limitations and
controls on the ability of you or any beneficiary to utilize the Dexorder Service. Such
limitations may include rejecting transaction requests, freezing funds in any case where
Dexorder has such ability, or otherwise restricting you from using the Dexorder Service, all
to the extent of our ability to do so.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">7. General Prohibitions and Dexorder's Enforcement Rights.</h2>
<div className="mb-4 leading-relaxed">You agree not to do any of the following:</div>
<div className="mb-4 leading-relaxed">
(a) Engage in or induce others to engage in any form of unauthorized access, hacking, or
social engineering, including without limitation any distributed denial or service or DDoS
attack, of Dexorder, the Dexorder Service, or any users of the foregoing;
</div>
<div className="mb-4 leading-relaxed">
(b) Use, display, mirror or frame the Dexorder Service or any individual element within the
Dexorder Service, Dexorder's name, any Dexorder trademark, logo or other proprietary
information, or the layout and design of any page or form contained on a page, without
Dexorder's express written consent;
</div>
<div className="mb-4 leading-relaxed">
(c) Access, tamper with, or use non-public areas of the Dexorder Service, Dexorder's
computer systems, or the technical delivery systems of Dexorder's providers;
</div>
<div className="mb-4 leading-relaxed">
(d) Attempt to probe, scan or test the vulnerability of any Dexorder system or network or
breach any security or authentication measures;
</div>
<div className="mb-4 leading-relaxed">
(e) Avoid, bypass, remove, deactivate, impair, descramble or otherwise circumvent any
technological measure implemented by Dexorder or any of Dexorder's providers or any other
third party (including another user) to protect the Dexorder Service;
</div>
<div className="mb-4 leading-relaxed">
(f) Attempt to access or search the Dexorder Service or download content from the Dexorder
Service using any engine, software, tool, agent, device or mechanism (including spiders,
robots, crawlers, data mining tools or the like) other than the software and/or search
agents provided by Dexorder or other generally available third-party web browsers;
</div>
<div className="mb-4 leading-relaxed">
(g) Use any meta tags or other hidden text or metadata utilizing a Dexorder trademark, logo,
URL or product name without Dexorder's express written consent;
</div>
<div className="mb-4 leading-relaxed">
(h) Forge any TCP/IP packet header or any part of the header information in any email or
newsgroup posting, or in any way use the Dexorder Service to send altered, deceptive or
false source-identifying information;
</div>
<div className="mb-4 leading-relaxed">
(i) Attempt to decipher, decompile, disassemble or reverse engineer any of the software used
to provide the Dexorder Service;
</div>
<div className="mb-4 leading-relaxed">
(j) Interfere with, or attempt to interfere with, the access of any user, host or network,
including, without limitation, sending a virus, exploiting any bug, overloading, flooding,
spamming, or mail- bombing the Dexorder Service;
</div>
<div className="mb-4 leading-relaxed">
(k) Use the Dexorder Service for benchmarking or analysis in a manner that could, directly
or indirectly, interfere with, detract from, or otherwise harm the Dexorder Service or DEX;
</div>
<div className="mb-4 leading-relaxed">
(l) Collect or store any personally identifiable information from the Dexorder Service from
other users of the Dexorder Service without their express permission;
</div>
<div className="mb-4 leading-relaxed">
(m) Impersonate or misrepresent your affiliation with any person or entity;
</div>
<div className="mb-4 leading-relaxed">
(n) Create or list any counterfeit items (including digital assets);
</div>
<div className="mb-4 leading-relaxed">
(o) Fabricate in any way any transaction or process related thereto;
</div>
<div className="mb-4 leading-relaxed">
(p) Engage or assist in any activity that violates any law, statute, ordinance, regulation,
or sanctions program, , or that involves proceeds of any unlawful activity (including but
not limited to money laundering, terrorist financing or deliberately engaging in activities
designed to adversely affect the performance of the Dexorder Service);
</div>
<div className="mb-4 leading-relaxed">(q) Engage in deceptive or manipulative trading activities;</div>
<div className="mb-4 leading-relaxed">
(r) Disguise or interfere in any way with the IP address of the computer you are using to
access or use the Dexorder Service or that otherwise prevents us from correctly identifying
the IP address of the computer you are using to access the Dexorder Service;
</div>
<div className="mb-4 leading-relaxed">
(s) Transmit, exchange, or otherwise support the direct or indirect proceeds of criminal or
fraudulent activity;
</div>
<div className="mb-4 leading-relaxed">(t) Violate any Applicable Law or regulation; or</div>
<div className="mb-4 leading-relaxed">
(u) Encourage or enable any other individual to do any of the foregoing.
</div>
<div className="mb-4 leading-relaxed">
Dexorder is not obligated to monitor access to or use of the Dexorder Service or to review
or edit any content. However, we have the right to do so for the purpose of operating the
Dexorder Service, to ensure compliance with these Terms and to comply with Applicable Law or
other legal requirements. We reserve the right, but are not obligated, to suspend or
terminate access to the Dexorder Service at any time if we believe you are violating these
Terms. We have the right to investigate violations of these Terms or conduct that affects
the Dexorder Service. We may also consult and cooperate with law enforcement authorities to
prosecute users who violate the law.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">8. Feedback</h2>
<div className="mb-4 leading-relaxed">
We appreciate feedback, comments, ideas, proposals and suggestions for improvements to the
Dexorder Service ("<b>Feedback</b>"). If you choose to submit Feedback, you agree that we
are free to use it (and permit others to use it) without any restriction or compensation to
you.
</div>
<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 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
third-party resources. Our provision of access to Third Party Resources does not constitute
approval, endorsement, or control of such Third Party Resource.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">10. Termination</h2>
<div className="mb-4 leading-relaxed">
We may suspend or terminate your access to and use of the Dexorder Service, at our sole
discretion, at any time and without notice to you. You acknowledge and agree that we shall
have no liability or obligation to you in such event and that you will not be entitled to a
refund of any amounts that you have already paid to us or any third party, to the fullest
extent permitted by Applicable Law. Upon any termination, discontinuation, or cancellation
of the Dexorder Service or your account, the following Sections will survive: 6.(d) , 7 , 8
, 10 , 11 , 13 , 14 , 15 , 16 , and 17 .
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">11. Warranty Disclaimers</h2>
<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 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
ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR USAGE OF TRADE. WE MAKE NO WARRANTY THAT
THE UTILITIES WILL MEET YOUR REQUIREMENTS, BE AVAILABLE ON AN UNINTERRUPTED, SECURE, OR
ERROR-FREE BASIS. WE MAKE NO WARRANTY REGARDING THE QUALITY, ACCURACY, TIMELINESS,
TRUTHFULNESS, COMPLETENESS OR RELIABILITY OF ANY INFORMATION OR CONTENT ON THE UTILITIES.
DEXORDER FURTHER EXPRESSLY DISCLAIMS ALL LIABILITY OR RESPONSIBILITY IN CONNECTION WITH
THIRD PARTY SERVICES. NOTHING HEREIN NOR ANY USE OF THE UTILITIES IN CONNECTION WITH THIRD
PARTY SERVICES CONSTITUTES OUR ENDORSEMENT, RECOMMENDATION OR ANY OTHER AFFILIATION OF OR
WITH ANY THIRD PARTY SERVICES.
</div>
<div className="mb-4 leading-relaxed">
DEXORDER DOES NOT REPRESENT OR WARRANT THAT ANY CONTENT ON THE UTILITIES IS ACCURATE,
COMPLETE, RELIABLE, CURRENT OR ERROR-FREE. WE WILL NOT BE LIABLE FOR ANY LOSS OF ANY KIND
FROM ANY ACTION TAKEN OR TAKEN IN RELIANCE ON MATERIAL OR INFORMATION CONTAINED ON THE
UTILITIES. DEXORDER CANNOT AND DOES NOT REPRESENT OR WARRANT THAT THE UTILITIES, ANY CONTENT
THEREIN, OR OUR SERVERS ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS.
</div>
<div className="mb-4 leading-relaxed">
WE CANNOT GUARANTEE THE SECURITY OF ANY DATA THAT YOU DISCLOSE ONLINE. YOU ACCEPT THE
INHERENT SECURITY RISKS OF PROVIDING INFORMATION AND DEALING ONLINE OVER THE INTERNET AND
WILL NOT HOLD US RESPONSIBLE FOR ANY BREACH OF SECURITY.
</div>
<div className="mb-4 leading-relaxed">
DEXORDER WILL NOT BE RESPONSIBLE OR LIABLE TO YOU FOR ANY LOSS AND TAKES NO RESPONSIBILITY
FOR, AND WILL NOT BE LIABLE TO YOU FOR, ANY USE OF THE UTILITIES, INCLUDING BUT NOT LIMITED
TO ANY LOSSES, DAMAGES OR CLAIMS ARISING FROM: (I) USER ERROR SUCH AS FORGOTTEN PASSWORDS,
INCORRECTLY CONSTRUCTED TRANSACTIONS, EXCEEDING TRANSFER LIMITS OF THIRD PARTY RESOURCES OR
THE DEX, OR MISTYPED WALLET ADDRESSES; (II) SERVER FAILURE OR DATA LOSS; (III) BLOCKCHAIN
NETWORKS, CRYPTOCURRENCY WALLETS, CORRUPT FILES, SOFTWARE ERRORS, OR BUGS; (IV) UNAUTHORIZED
ACCESS TO THE UTILITIES; OR (V) ANY THIRD PARTY UTILITIES, INCLUDING WITHOUT LIMITATION THE
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 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,
VIRUSES, DENIAL OF SERVICE OR OTHER ATTACKS, TECHNICAL FAILURE OF THE UTILITIES AND/OR
TELECOMMUNICATIONS INFRASTRUCTURE OR DISRUPTION, AND THEREFORE WE EXPRESSLY DISCLAIM ANY
EXPRESS OR IMPLIED WARRANTY REGARDING THE USE AND/OR AVAILABILITY, ACCESSIBILITY, SECURITY
OR PERFORMANCE OF THE UTILITIES CAUSED BY SUCH FACTORS. WE DO NOT MAKE ANY REPRESENTATIONS
OR WARRANTIES AGAINST THE POSSIBILITY OF DELETION, MISDELIVERY OR FAILURE TO STORE
COMMUNICATIONS, PERSONALIZED SETTINGS OR OTHER DATA. SOME JURISDICTIONS DO NOT ALLOW THE
EXCLUSION OF CERTAIN WARRANTIES. ACCORDINGLY, SOME OF THE ABOVE DISCLAIMERS OF WARRANTIES
MAY NOT APPLY TO YOU BUT OTHERS REMAIN IN EFFECT.
</div>
<div className="mb-4 leading-relaxed">
You understand that your use of the Utilities is entirely at your own risk. You assume all
risks associated with using the Utilities, and digital assets and decentralized systems
generally, including but not limited to, that digital assets are highly volatile; you may
not have ready access to assets; and you may lose some or all of your tokens or other
assets, including with respect to an Interaction or the Vault. You agree that you will have
no recourse against Dexorder for any losses due to your use of the Utilities. For example,
these losses may arise from or relate to: (i) lost funds; (ii) server failure or data loss;
(iii) corrupted digital wallet files; (iv) unauthorized access; (v) errors, mistakes, or
inaccuracies; or (vi) third-party activities.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">12. Assumption of Risk.</h2>
<div className="mb-4 leading-relaxed">You accept, acknowledge and assume the following risks:</div>
<div className="mb-4 leading-relaxed">
(a) You are solely responsible for determining what, if any, Taxes apply to your
transactions through the Utilities. Neither Dexorder nor any Dexorder affiliates are
responsible for determining the Taxes that apply to such transactions.
</div>
<div className="mb-4 leading-relaxed">
(b) A lack of use or public interest in the creation and development of distributed
ecosystems could negatively impact the development of those ecosystems and related
applications, and could therefore also negatively impact the potential utility or value of
certain digital assets.
</div>
<div className="mb-4 leading-relaxed">
(c) By accessing and using the Utilities, you represent that you understand the inherent
risks associated with using cryptographic and blockchain-based systems, and that you have a
working knowledge of the usage and intricacies of tokens such as, bitcoin (BTC), ether
(ETH), and other digital tokens such as those following the Ethereum Token Standard
(ERC-20). You further understand that the markets for tokens can be highly volatile due to
factors including (but not limited to) adoption, speculation, technology, security, and
regulation. You acknowledge that the cost and speed of transacting with cryptographic and
blockchain-based systems are variable and may increase at any time. Accordingly, you
understand and agree to assume full responsibility for all of the risks of accessing and
using and engaging with the Utilities.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">13. Indemnity</h2>
<div className="mb-4 leading-relaxed">
You will indemnify, defend (at Dexorder's option) and hold Dexorder and its affiliates and
their respective officers, directors, employees and agents, harmless from and against any
claims, disputes, demands, liabilities, damages, losses, and costs and expenses, including,
without limitation, reasonable legal and accounting fees arising out of or in any way
connected with: (a) your access to or use of the Utilities, (b) Interactions and the Vault,
(c) your violation of these Terms, or (d) your negligence, willful misconduct, fraud, or
violation of Applicable Laws. You may not settle or otherwise compromise any claim subject
to this Section without Dexorder's prior written approval.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">14. Limitation of Liability</h2>
<div className="mb-4 leading-relaxed">
(a) TO THE MAXIMUM EXTENT PERMITTED BY LAW, NEITHER DEXORDER NOR ITS SERVICE PROVIDERS
INVOLVED IN CREATING, PRODUCING, OR DELIVERING THE UTILITIES WILL BE LIABLE FOR ANY
INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES, OR DAMAGES FOR LOST PROFITS, LOST
REVENUES, LOST SAVINGS, LOST BUSINESS OPPORTUNITY, LOSS OF DATA OR GOODWILL, SERVICE
INTERRUPTION, COMPUTER DAMAGE OR SYSTEM FAILURE OR THE COST OF SUBSTITUTE SERVICES OF ANY
KIND ARISING OUT OF OR IN CONNECTION WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE
THE UTILITIES, WHETHER BASED ON WARRANTY, CONTRACT, TORT (INCLUDING NEGLIGENCE), PRODUCT
LIABILITY OR ANY OTHER LEGAL THEORY, AND WHETHER OR NOT DEXORDER OR ITS SERVICE PROVIDERS
HAS BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGE, EVEN IF A LIMITED REMEDY SET FORTH
HEREIN IS FOUND TO HAVE FAILED OF ITS ESSENTIAL PURPOSE.
</div>
<div className="mb-4 leading-relaxed">
(b) TO THE MAXIMUM EXTENT PERMITTED BY THE LAW, IN NO EVENT WILL DEXORDER'S TOTAL LIABILITY
ARISING OUT OF OR IN CONNECTION WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE THE
UTILITIES EXCEED THE TOTAL FEES YOU HAVE PAID OR ARE PAYABLE BY YOU TO DEXORDER FOR USE OF
THE UTILITIES (EXCLUDING GAS FEES), OR ONE HUNDRED DOLLARS ($100) IF YOU HAVE NOT HAD ANY
PAYMENT OBLIGATIONS TO DEXORDER, AS APPLICABLE.
</div>
<div className="mb-4 leading-relaxed">
(c) THE EXCLUSIONS AND LIMITATIONS OF DAMAGES SET FORTH ABOVE ARE FUNDAMENTAL ELEMENTS OF
THE BASIS OF THE BARGAIN BETWEEN DEXORDER AND YOU.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">15. Governing Law and Forum Choice</h2>
<div className="mb-4 leading-relaxed">
These Terms will be governed by and construed in accordance with the laws of the British
Virgin Islands without regard to its conflict of laws provisions.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">16. Dispute Resolution</h2>
<div className="mb-4 leading-relaxed">
(a) <u>Mandatory Arbitration of Disputes</u>. We each agree that any dispute, claim or
controversy arising out of or relating to these Terms or the breach, termination,
enforcement, interpretation or validity thereof or the use of the Utilities (collectively, "
<b>Disputes</b>") will be resolved{' '}
<b>
solely by binding, individual arbitration and not in a class, representative or
consolidated action or proceeding.
</b>{' '}
You and Dexorder agree that the Cayman Islands Arbitration Law governs the interpretation
and enforcement of these Terms, and that you and Dexorder are each waiving the right to a
trial by jury or to participate in a class action. This arbitration provision shall survive
termination of these Terms.
</div>
<div className="mb-4 leading-relaxed">
(b) <u>Exceptions</u>. As limited exceptions to Section 16.(a) above: (i) each party may
seek to resolve a Dispute in small claims court if it qualifies; and (ii) each party retains
the right to seek injunctive or other equitable relief from a court to prevent (or enjoin)
the infringement or misappropriation of our intellectual property rights.
</div>
<div className="mb-4 leading-relaxed">
(c) <u>Conducting Arbitration and Arbitration Rules</u>. The arbitration will be conducted
by the Cayman International Mediation &amp; Arbitration Centre (CI-MAC) in accordance with
its arbitration rules in force at the time of the dispute ("<b>CI-MAC Rules</b>"), except as
modified by these Terms. The CI-MAC Rules are available at{' '}
<a href="https://www.caymanarbitration.com/arbitrationrules2023">
https://www.caymanarbitration.com/arbitrationrules2023
</a>
. A party who wishes to start arbitration must submit a written request for arbitration to
CI-MAC and give notice to the other party as specified in the CI-MAC Rules. CI-MAC provides
instructions on submitting a request for arbitration under Section 3 (Request for
arbitration) of the CI-MAC Rules.
</div>
<div className="mb-4 leading-relaxed">
Any arbitration hearings will take place in the county (or parish) where you live, unless
the parties agree to a different location. The parties agree that the arbitrator shall have
exclusive authority to decide all issues relating to the interpretation, applicability,
enforceability and scope of this arbitration agreement.
</div>
<div className="mb-4 leading-relaxed">
(d) <u>Arbitration Costs</u>. Payment of all filing, administration and arbitrator fees will
be governed by the JAMS Rules, and we won't seek to recover the administration and
arbitrator fees we are responsible for paying, unless the arbitrator finds your Dispute
frivolous. If we prevail in arbitration we'll pay all of our attorneys' fees and costs and
won't seek to recover them from you. If you prevail in arbitration you will be entitled to
an award of attorneys' fees and expenses to the extent provided under Applicable Law.
</div>
<div className="mb-4 leading-relaxed">
(e) <u>Injunctive and Declaratory Relief</u>. Except as provided in Section 16.(b) above,
the arbitrator shall determine all issues of liability on the merits of any claim asserted
by either party and may award declaratory or injunctive relief only in favor of the
individual party seeking relief and only to the extent necessary to provide relief warranted
by that party's individual claim. To the extent that you or we prevail on a claim and seek
public injunctive relief (that is, injunctive relief that has the primary purpose and effect
of prohibiting unlawful acts that threaten future injury to the public), the entitlement to
and extent of such relief must be litigated in a civil court of competent jurisdiction and
not in arbitration. The parties agree that litigation of any issues of public injunctive
relief shall be stayed pending the outcome of the merits of any individual claims in
arbitration.
</div>
<div className="mb-4 leading-relaxed">
(f) <u>Class Action Waiver</u>.{' '}
<b>
YOU AND DEXORDER AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS
INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR
REPRESENTATIVE PROCEEDING.
</b>{' '}
Further, if the parties' Dispute is resolved through arbitration, the arbitrator may not
consolidate another person's claims with your claims, and may not otherwise preside over any
form of a representative or class proceeding. If this specific provision is found to be
unenforceable, then the entirety of this Dispute Resolution section shall be null and void.
</div>
<div className="mb-4 leading-relaxed">
(g) <u>Severability</u>. With the exception of any of the provisions in Section 16.(f) of
these Terms ("<b>Class Action Waiver</b>"), if an arbitrator or court of competent
jurisdiction decides that any part of these Terms is invalid or unenforceable, the other
parts of these Terms will still apply.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">17. General Terms</h2>
<div className="mb-4 leading-relaxed">
(a) <u>Reservation of Rights</u>. Dexorder and its licensors exclusively own all right,
title and interest in and to the Dexorder Service, including all associated intellectual
property rights. You acknowledge that the Dexorder Service is protected by copyright,
trademark, and other laws of the United States and foreign countries. You agree not to
remove, alter or obscure any copyright, trademark, service mark or other proprietary rights
notices incorporated in or accompanying the Dexorder Service.
</div>
<div className="mb-4 leading-relaxed">
(b) <u>Entire Agreement</u>. These Terms, including any addendum terms, constitute the
entire and exclusive understanding and agreement between Dexorder and you regarding the
Dexorder Service, and these Terms supersede and replace all prior oral or written
understandings or agreements between Dexorder and you regarding the Dexorder Service. If any
provision of these Terms is held invalid or unenforceable by an arbitrator or a court of
competent jurisdiction, that provision will be enforced to the maximum extent permissible
and the other provisions of these Terms will remain in full force and effect. Except where
provided by Applicable Law in your jurisdiction, you may not assign or transfer these Terms,
by operation of law or otherwise, without Dexorder's prior written consent. Any attempt by
you to assign or transfer these Terms absent our consent or your statutory right, will be
null. Dexorder may freely assign or transfer these Terms without restriction. Subject to the
foregoing, these Terms will bind and inure to the benefit of the parties, their successors
and permitted assigns.
</div>
<div className="mb-4 leading-relaxed">
(c) <u>Notices</u>. Any notices or other communications provided by Dexorder under these
Terms will be given: (i) via email; or (ii) by posting to the Dexorder Service. For notices
made by email, the date of receipt will be deemed the date on which such notice is
transmitted.
</div>
<div className="mb-4 leading-relaxed">
(d) <u>Waiver of Rights</u>. Dexorder's failure to enforce any right or provision of these
Terms will not be considered a waiver of such right or provision. The waiver of any such
right or provision will be effective only if in writing and signed by a duly authorized
representative of Dexorder. Except as expressly set forth in these Terms, the exercise by
either party of any of its remedies under these Terms will be without prejudice to its other
remedies under these Terms or otherwise.
</div>
<h2 className="text-xl font-semibold mt-6 mb-4">18. Contact Information</h2>
<div className="mb-4 leading-relaxed">
If you have any questions about these Terms or the Dexorder Service, please contact Dexorder
at <a href="mailto:legal@dexorder.com">legal@dexorder.com</a> or{' '}
<a href="mailto:support@dexorder.com">support@dexorder.com</a>.
</div>
</div>
</div>
);
}

102
src/components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,102 @@
'use client';
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { X, Loader2, CheckCircle2 } from 'lucide-react';
interface Toast {
id: string;
title: string;
description?: string;
type?: 'loading' | 'success' | 'error';
}
interface ToastContextType {
toasts: Toast[];
addToast: (toast: Omit<Toast, 'id'>) => string;
removeToast: (id: string) => void;
updateToast: (id: string, toast: Partial<Omit<Toast, 'id'>>) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
const id = Math.random().toString(36).substr(2, 9);
setToasts((prev) => [...prev, { ...toast, id }]);
// Auto remove after 5 seconds for success/error toasts
if (toast.type === 'success' || toast.type === 'error') {
setTimeout(() => {
removeToast(id);
}, 5000);
}
return id;
}, []);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
const updateToast = useCallback((id: string, updates: Partial<Omit<Toast, 'id'>>) => {
setToasts((prev) =>
prev.map((toast) =>
toast.id === id ? { ...toast, ...updates } : toast
)
);
// Auto remove if updated to success/error
if (updates.type === 'success' || updates.type === 'error') {
setTimeout(() => {
removeToast(id);
}, 5000);
}
}, [removeToast]);
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast, updateToast }}>
{children}
<div className="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 max-w-md">
{toasts.map((toast) => (
<div
key={toast.id}
className="bg-background border rounded-lg shadow-lg p-4 flex items-start gap-3 animate-in slide-in-from-right"
>
{toast.type === 'loading' && (
<Loader2 className="h-5 w-5 animate-spin text-primary mt-0.5" />
)}
{toast.type === 'success' && (
<CheckCircle2 className="h-5 w-5 text-green-500 mt-0.5" />
)}
<div className="flex-1">
<div className="font-semibold text-sm">{toast.title}</div>
{toast.description && (
<div className="text-sm text-muted-foreground mt-1">
{toast.description}
</div>
)}
</div>
<button
onClick={() => removeToast(toast.id)}
className="rounded-sm opacity-70 hover:opacity-100 transition-opacity"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
}
export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within ToastProvider');
}
return context;
}

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>
);
}

30
src/contracts/ERC20ABI.ts Normal file
View File

@@ -0,0 +1,30 @@
export const ERC20ABI = [
{
type: 'function',
name: 'name',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'string' }],
},
{
type: 'function',
name: 'symbol',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'string' }],
},
{
type: 'function',
name: 'decimals',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'uint8' }],
},
{
type: 'function',
name: 'balanceOf',
stateMutability: 'view',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
},
] as const;

View File

@@ -0,0 +1,296 @@
/* GENERATED FILE: DO NOT EDIT! */
const IPartyInfoABI = [
{
"type": "function",
"name": "burnAmounts",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "lpTokenAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "withdrawAmounts",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "burnSwapAmounts",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "lpAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "outputTokenIndex",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "amountOut",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "outFee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "flashFee",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "token",
"type": "address",
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "fee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "maxFlashLoan",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "token",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "mintAmounts",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "lpTokenAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "depositAmounts",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "poolPrice",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "quoteTokenIndex",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "int128",
"internalType": "int128"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "price",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "baseTokenIndex",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "quoteTokenIndex",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "swapMintAmounts",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "inputTokenIndex",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "maxAmountIn",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "amountInUsed",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "lpMinted",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "inFee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "swapToLimitAmounts",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "inputTokenIndex",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "outputTokenIndex",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "limitPrice",
"type": "int128",
"internalType": "int128"
}
],
"outputs": [
{
"name": "amountIn",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amountOut",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "inFee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "working",
"inputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
}
] as const;
export default IPartyInfoABI;

View File

@@ -115,42 +115,37 @@ const IPartyPlannerABI = [
"name": "newPool",
"inputs": [
{
"name": "name_",
"name": "name",
"type": "string",
"internalType": "string"
},
{
"name": "symbol_",
"name": "symbol",
"type": "string",
"internalType": "string"
},
{
"name": "_tokens",
"name": "tokens",
"type": "address[]",
"internalType": "contract IERC20[]"
},
{
"name": "_bases",
"type": "uint256[]",
"internalType": "uint256[]"
},
{
"name": "_kappa",
"name": "kappa",
"type": "int128",
"internalType": "int128"
},
{
"name": "_swapFeePpm",
"name": "swapFeesPpm",
"type": "uint256[]",
"internalType": "uint256[]"
},
{
"name": "flashFeePpm",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "_flashFeePpm",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "_stable",
"name": "stable",
"type": "bool",
"internalType": "bool"
},
@@ -199,47 +194,42 @@ const IPartyPlannerABI = [
"name": "newPool",
"inputs": [
{
"name": "name_",
"name": "name",
"type": "string",
"internalType": "string"
},
{
"name": "symbol_",
"name": "symbol",
"type": "string",
"internalType": "string"
},
{
"name": "_tokens",
"name": "tokens",
"type": "address[]",
"internalType": "contract IERC20[]"
},
{
"name": "_bases",
"type": "uint256[]",
"internalType": "uint256[]"
},
{
"name": "_tradeFrac",
"name": "tradeFrac",
"type": "int128",
"internalType": "int128"
},
{
"name": "_targetSlippage",
"name": "targetSlippage",
"type": "int128",
"internalType": "int128"
},
{
"name": "_swapFeePpm",
"name": "swapFeePpm",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "_flashFeePpm",
"name": "flashFeePpm",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "_stable",
"name": "stable",
"type": "bool",
"internalType": "bool"
},
@@ -283,6 +273,98 @@ const IPartyPlannerABI = [
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "newPool",
"inputs": [
{
"name": "name",
"type": "string",
"internalType": "string"
},
{
"name": "symbol",
"type": "string",
"internalType": "string"
},
{
"name": "tokens",
"type": "address[]",
"internalType": "contract IERC20[]"
},
{
"name": "kappa",
"type": "int128",
"internalType": "int128"
},
{
"name": "swapFeePpm",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "flashFeePpm",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "stable",
"type": "bool",
"internalType": "bool"
},
{
"name": "payer",
"type": "address",
"internalType": "address"
},
{
"name": "receiver",
"type": "address",
"internalType": "address"
},
{
"name": "initialDeposits",
"type": "uint256[]",
"internalType": "uint256[]"
},
{
"name": "initialLpAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "deadline",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "pool",
"type": "address",
"internalType": "contract IPartyPool"
},
{
"name": "lpAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "owner",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "poolCount",
@@ -317,7 +399,14 @@ const IPartyPlannerABI = [
},
{
"type": "function",
"name": "swapMintImpl",
"name": "renounceOwnership",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "swapImpl",
"inputs": [],
"outputs": [
{
@@ -341,6 +430,38 @@ const IPartyPlannerABI = [
],
"stateMutability": "view"
},
{
"type": "function",
"name": "transferOwnership",
"inputs": [
{
"name": "newOwner",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "event",
"name": "OwnershipTransferred",
"inputs": [
{
"name": "previousOwner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "newOwner",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"type": "event",
"name": "PartyStarted",
@@ -371,6 +492,28 @@ const IPartyPlannerABI = [
}
],
"anonymous": false
},
{
"type": "error",
"name": "OwnableInvalidOwner",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
}
]
},
{
"type": "error",
"name": "OwnableUnauthorizedAccount",
"inputs": [
{
"name": "account",
"type": "address",
"internalType": "address"
}
]
}
] as const;

View File

@@ -11,11 +11,6 @@ const IPartyPoolABI = [
"type": "tuple",
"internalType": "struct LMSRStabilized.State",
"components": [
{
"name": "nAssets",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "kappa",
"type": "int128",
@@ -147,6 +142,11 @@ const IPartyPoolABI = [
"name": "deadline",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "unwrap",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [
@@ -178,7 +178,7 @@ const IPartyPoolABI = [
"internalType": "uint256"
},
{
"name": "inputTokenIndex",
"name": "outputTokenIndex",
"type": "uint256",
"internalType": "uint256"
},
@@ -186,11 +186,21 @@ const IPartyPoolABI = [
"name": "deadline",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "unwrap",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [
{
"name": "amountOutUint",
"name": "amountOut",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "outFee",
"type": "uint256",
"internalType": "uint256"
}
@@ -230,6 +240,43 @@ const IPartyPoolABI = [
],
"stateMutability": "view"
},
{
"type": "function",
"name": "fee",
"inputs": [
{
"name": "i",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "j",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "fees",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "flashFeePpm",
@@ -277,25 +324,6 @@ const IPartyPoolABI = [
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "getToken",
"inputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IERC20"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "initialMint",
@@ -318,7 +346,7 @@ const IPartyPoolABI = [
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
"stateMutability": "payable"
},
{
"type": "function",
@@ -333,6 +361,26 @@ const IPartyPoolABI = [
],
"stateMutability": "view"
},
{
"type": "function",
"name": "kill",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "killed",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "mint",
@@ -365,7 +413,20 @@ const IPartyPoolABI = [
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
"stateMutability": "payable"
},
{
"type": "function",
"name": "mintImpl",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
@@ -393,6 +454,19 @@ const IPartyPoolABI = [
],
"stateMutability": "view"
},
{
"type": "function",
"name": "owner",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "protocolFeeAddress",
@@ -419,6 +493,13 @@ const IPartyPoolABI = [
],
"stateMutability": "view"
},
{
"type": "function",
"name": "renounceOwnership",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "swap",
@@ -428,6 +509,11 @@ const IPartyPoolABI = [
"type": "address",
"internalType": "address"
},
{
"name": "fundingSelector",
"type": "bytes4",
"internalType": "bytes4"
},
{
"name": "receiver",
"type": "address",
@@ -457,6 +543,16 @@ const IPartyPoolABI = [
"name": "deadline",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "unwrap",
"type": "bool",
"internalType": "bool"
},
{
"name": "cbData",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [
@@ -471,22 +567,66 @@ const IPartyPoolABI = [
"internalType": "uint256"
},
{
"name": "fee",
"name": "inFee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
"stateMutability": "payable"
},
{
"type": "function",
"name": "swapFeePpm",
"name": "swapAmounts",
"inputs": [
{
"name": "inputTokenIndex",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "outputTokenIndex",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "maxAmountIn",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "limitPrice",
"type": "int128",
"internalType": "int128"
}
],
"outputs": [
{
"name": "amountIn",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amountOut",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "inFee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "swapImpl",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
@@ -522,13 +662,23 @@ const IPartyPoolABI = [
}
],
"outputs": [
{
"name": "amountInUsed",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "lpMinted",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "inFee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
"stateMutability": "payable"
},
{
"type": "function",
@@ -539,6 +689,11 @@ const IPartyPoolABI = [
"type": "address",
"internalType": "address"
},
{
"name": "fundingSelector",
"type": "bytes4",
"internalType": "bytes4"
},
{
"name": "receiver",
"type": "address",
@@ -563,6 +718,16 @@ const IPartyPoolABI = [
"name": "deadline",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "unwrap",
"type": "bool",
"internalType": "bool"
},
{
"name": "cbData",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [
@@ -577,12 +742,12 @@ const IPartyPoolABI = [
"internalType": "uint256"
},
{
"name": "fee",
"name": "inFee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
"stateMutability": "payable"
},
{
"type": "function",
@@ -597,6 +762,25 @@ const IPartyPoolABI = [
],
"stateMutability": "view"
},
{
"type": "function",
"name": "token",
"inputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IERC20"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "totalSupply",
@@ -663,6 +847,32 @@ const IPartyPoolABI = [
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "transferOwnership",
"inputs": [
{
"name": "newOwner",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "wrapperToken",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract NativeWrapper"
}
],
"stateMutability": "view"
},
{
"type": "event",
"name": "Approval",
@@ -736,13 +946,31 @@ const IPartyPoolABI = [
"internalType": "address"
},
{
"name": "targetTokenIndex",
"type": "uint256",
"name": "tokenOut",
"type": "address",
"indexed": true,
"internalType": "contract IERC20"
},
{
"name": "amountIn",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "payoutUint",
"name": "amountOut",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "lpFee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "protocolFee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
@@ -750,6 +978,55 @@ const IPartyPoolABI = [
],
"anonymous": false
},
{
"type": "event",
"name": "Flash",
"inputs": [
{
"name": "initiator",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "receiver",
"type": "address",
"indexed": true,
"internalType": "contract IERC3156FlashBorrower"
},
{
"name": "token",
"type": "address",
"indexed": true,
"internalType": "contract IERC20"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "lpFee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "protocolFee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "Killed",
"inputs": [],
"anonymous": false
},
{
"type": "event",
"name": "Mint",
@@ -781,6 +1058,31 @@ const IPartyPoolABI = [
],
"anonymous": false
},
{
"type": "event",
"name": "OwnershipTransferred",
"inputs": [
{
"name": "previousOwner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "newOwner",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"type": "event",
"name": "ProtocolFeesCollected",
"inputs": [],
"anonymous": false
},
{
"type": "event",
"name": "Swap",
@@ -820,6 +1122,18 @@ const IPartyPoolABI = [
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "lpFee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "protocolFee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
@@ -841,25 +1155,31 @@ const IPartyPoolABI = [
"internalType": "address"
},
{
"name": "inputTokenIndex",
"type": "uint256",
"name": "tokenIn",
"type": "address",
"indexed": true,
"internalType": "uint256"
"internalType": "contract IERC20"
},
{
"name": "grossTransfer",
"name": "amountIn",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "netInput",
"name": "amountOut",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "feeTaken",
"name": "lpFee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "protocolFee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
@@ -891,6 +1211,28 @@ const IPartyPoolABI = [
}
],
"anonymous": false
},
{
"type": "error",
"name": "OwnableInvalidOwner",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
}
]
},
{
"type": "error",
"name": "OwnableUnauthorizedAccount",
"inputs": [
{
"name": "account",
"type": "address",
"internalType": "address"
}
]
}
] as const;

View File

@@ -40,7 +40,7 @@ const IPartyPoolViewerABI = [
"internalType": "uint256"
},
{
"name": "inputTokenIndex",
"name": "outputTokenIndex",
"type": "uint256",
"internalType": "uint256"
}
@@ -50,6 +50,11 @@ const IPartyPoolViewerABI = [
"name": "amountOut",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "outFee",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
@@ -235,12 +240,12 @@ const IPartyPoolViewerABI = [
"internalType": "uint256"
},
{
"name": "fee",
"name": "lpMinted",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "lpMinted",
"name": "inFee",
"type": "uint256",
"internalType": "uint256"
}
@@ -284,7 +289,7 @@ const IPartyPoolViewerABI = [
"internalType": "uint256"
},
{
"name": "fee",
"name": "inFee",
"type": "uint256",
"internalType": "uint256"
}

View File

@@ -2,10 +2,26 @@
import { useState, useEffect } from 'react';
import { usePublicClient } from 'wagmi';
import chainInfo from '@/app/liquidity-party.json';
import chainInfo from '@/contracts/liqp-deployments.json';
import IPartyPlannerABI from '@/contracts/IPartyPlannerABI';
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
import IPartyPoolViewerABI from '@/contracts/IPartyPoolViewerABI';
import IPartyInfoABI from '@/contracts/IPartyInfoABI';
import { ERC20ABI } from '@/contracts/ERC20ABI';
// Helper function to format large numbers with K, M, B suffixes
function formatTVL(value: number): string {
if (value >= 1_000_000_000) {
return `$${(value / 1_000_000_000).toFixed(2)}B`;
} else if (value >= 1_000_000) {
return `$${(value / 1_000_000).toFixed(2)}M`;
} else if (value >= 1_000) {
return `$${(value / 1_000).toFixed(2)}K`;
} else {
return `$${value.toFixed(2)}`;
}
}
export function useGetAllTokens(offset: number = 0, limit: number = 100) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
@@ -33,7 +49,8 @@ export function useGetAllTokens(offset: number = 0, limit: number = 100) {
// Get chain ID and contract address
const chainId = await publicClient.getChainId();
const address = chainInfo[chainId.toString()]?.IPartyPlanner;
// @ts-ignore
const address = (chainInfo as Record<string, { v1: { PartyPlanner: string } }>)[chainId.toString()]?.v1?.PartyPlanner;
if (!address) {
setError('IPartyPlanner contract not found for current chain');
@@ -43,7 +60,7 @@ export function useGetAllTokens(offset: number = 0, limit: number = 100) {
// Call getAllTokens function
const result = await publicClient.readContract({
address,
address: address as `0x${string}`,
abi: IPartyPlannerABI,
functionName: 'getAllTokens',
args: [BigInt(offset), BigInt(limit)],
@@ -51,6 +68,7 @@ export function useGetAllTokens(offset: number = 0, limit: number = 100) {
setTokens(result);
} catch (err) {
console.error('Error calling getAllTokens:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch tokens');
} finally {
setLoading(false);
@@ -73,7 +91,259 @@ export interface TokenDetails {
name: string;
symbol: string;
decimals: number;
balance: bigint;P
balance: bigint;
index: number;
}
export interface SwapRoute {
poolAddress: `0x${string}`;
inputTokenIndex: number;
outputTokenIndex: number;
inputTokenDecimal: number;
outputTokenDecimal: number;
}
export interface AvailableToken {
address: `0x${string}`;
symbol: string;
swapRoutes: SwapRoute[];
}
export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offset: number = 0, limit: number = 100) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
const [availableTokens, setAvailableTokens] = useState<AvailableToken[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Handle hydration for Next.js static export
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted || !tokenAddress) {
setLoading(false);
return;
}
const fetchPoolsFromTokens = async () => {
if (!publicClient) {
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Get chain ID and contract addresses
const chainId = await publicClient.getChainId();
const plannerAddress = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyPlanner;
const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!plannerAddress || !partyInfoAddress) {
setError('IPartyPlanner or PartyInfo contract not found for current chain');
setAvailableTokens([]);
return;
}
// Call getPoolsByToken function
const poolsResult = await publicClient.readContract({
address: plannerAddress as `0x${string}`,
abi: IPartyPlannerABI,
functionName: 'getPoolsByToken',
args: [tokenAddress, BigInt(offset), BigInt(limit)],
});
// Get the symbol of the originally selected token
const selectedTokenSymbol = await publicClient.readContract({
address: tokenAddress,
abi: ERC20ABI,
functionName: 'symbol',
}).catch(() => null);
if (!selectedTokenSymbol) {
setAvailableTokens([]);
return;
}
// Filter pools to only working ones
const workingPools: `0x${string}`[] = [];
for (const poolAddress of poolsResult) {
try {
const isWorking = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'working',
args: [poolAddress],
}) as boolean;
if (isWorking) {
workingPools.push(poolAddress);
}
} catch (err) {
console.error(`Error checking if pool ${poolAddress} is working:`, err);
}
}
// If no working pools found, set error message
if (workingPools.length === 0 && poolsResult.length > 0) {
setError('This token is no longer supported. All pools containing this token are currently inactive.');
setAvailableTokens([]);
return;
} else if (workingPools.length === 0) {
setError('No pools found for this token.');
setAvailableTokens([]);
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, process tokens
for (let poolIdx = 0; poolIdx < workingPools.length; poolIdx++) {
const poolAddress = workingPools[poolIdx];
const poolTokensResult = poolTokensResults[poolIdx];
if (poolTokensResult.status !== 'success') {
console.error('Failed to fetch tokens for pool', poolAddress);
continue;
}
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;
}
const outputTokenData = tokenDataMap.get(outputTokenAddress.toLowerCase());
const outputTokenSymbol = outputTokenData?.symbol ?? null;
const outputTokenDecimal = outputTokenData?.decimals ?? null;
// Skip tokens with the same symbol as the selected token
if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) {
continue;
}
// Skip tokens if decimals failed to load
if (inputTokenDecimal === null || outputTokenDecimal === null) {
console.error(`Failed to load decimals for token ${outputTokenAddress} or ${tokenAddress}`);
continue;
}
// Create or update the available token entry
const tokenKey = outputTokenAddress.toLowerCase();
if (!tokenRoutesMap.has(tokenKey)) {
tokenRoutesMap.set(tokenKey, {
address: outputTokenAddress,
symbol: outputTokenSymbol,
swapRoutes: [],
});
}
// Add this swap route
tokenRoutesMap.get(tokenKey)!.swapRoutes.push({
poolAddress,
inputTokenIndex,
outputTokenIndex,
inputTokenDecimal,
outputTokenDecimal,
});
}
}
const availableTokensList = Array.from(tokenRoutesMap.values());
setAvailableTokens(availableTokensList);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch pools and tokens');
} finally {
setLoading(false);
}
};
fetchPoolsFromTokens();
}, [publicClient, mounted, tokenAddress, offset, limit]);
return {
availableTokens,
loading,
error,
isReady: mounted,
};
}
export function useTokenDetails(userAddress: `0x${string}` | undefined) {
@@ -99,52 +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[] = [];
for (let i = 0; i < tokens.length; i++) {
const baseIndex = i * 4;
const nameResult = results[baseIndex];
const symbolResult = results[baseIndex + 1];
const decimalsResult = results[baseIndex + 2];
const balanceResult = results[baseIndex + 3];
// Make individual calls for each token
for (const tokenAddress of tokens) {
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)),
]);
details.push({
address: tokenAddress,
name: name as string,
symbol: symbol as string,
decimals: Number(decimals),
balance: balance as bigint,
});
} catch (err) {
// Add token with fallback values if individual call fails
details.push({
address: tokenAddress,
name: 'Unknown',
symbol: '???',
decimals: 18,
balance: BigInt(0),
});
}
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);
@@ -163,4 +435,543 @@ export function useTokenDetails(userAddress: `0x${string}` | undefined) {
loading,
error,
};
}
export interface PoolDetails {
address: `0x${string}`;
name: string;
symbol: string;
tokens: readonly `0x${string}`[];
price?: string; // Formatted price string
tvl?: string; // Formatted TVL string (e.g., "$1.2M")
isKilled: boolean; // Whether the pool has been killed
}
export function useGetAllPools(offset: number = 0, limit: number = 100) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
const [pools, setPools] = useState<readonly `0x${string}`[] | null>(null);
const [poolDetails, setPoolDetails] = useState<PoolDetails[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Handle hydration for Next.js static export
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted) return;
const fetchPools = async () => {
if (!publicClient) {
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Get chain ID and contract addresses
const chainId = await publicClient.getChainId();
const plannerAddress = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyPlanner;
const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!plannerAddress || !partyInfoAddress) {
setError('IPartyPlanner or PartyInfo contract not found for current chain');
setPools([]);
setPoolDetails([]);
return;
}
// Call getAllPools function
const result = await publicClient.readContract({
address: plannerAddress as `0x${string}`,
abi: IPartyPlannerABI,
functionName: 'getAllPools',
args: [BigInt(offset), BigInt(limit)],
});
setPools(result);
// Fetch details for each pool and check if it's working
const details: PoolDetails[] = [];
for (const poolAddress of result) {
try {
const [name, symbol, tokens, isWorking] = await Promise.all([
publicClient.readContract({
address: poolAddress,
abi: ERC20ABI,
functionName: 'name',
}).catch(() => 'Unknown Pool'),
publicClient.readContract({
address: poolAddress,
abi: ERC20ABI,
functionName: 'symbol',
}).catch(() => 'POOL'),
publicClient.readContract({
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'allTokens',
}).catch(() => [] as readonly `0x${string}`[]),
publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'working',
args: [poolAddress],
}).catch(() => false),
]);
// Fetch pool price and TVL (only for working pools)
let priceStr: string | undefined;
let tvlStr: string | undefined;
if (isWorking) {
// 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, balance, and pool price in parallel
const [decimals, balance, priceRaw] = await Promise.all([
publicClient.readContract({
address: firstTokenAddress as `0x${string}`,
abi: ERC20ABI,
functionName: 'decimals',
}) as Promise<number>,
publicClient.readContract({
address: firstTokenAddress as `0x${string}`,
abi: ERC20ABI,
functionName: 'balanceOf',
args: [poolAddress],
}) as Promise<bigint>,
publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyInfoABI,
functionName: 'poolPrice',
args: [poolAddress, BigInt(0)],
}),
]);
// 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 pool price/TVL for ${poolAddress}:`, err);
priceStr = undefined;
tvlStr = undefined;
}
}
// Add all pools (both working and killed)
details.push({
address: poolAddress,
name: name as string,
symbol: symbol as string,
tokens: tokens as readonly `0x${string}`[],
price: priceStr,
tvl: tvlStr,
isKilled: !isWorking,
});
} catch (err) {
console.error('Error fetching pool details for', poolAddress, err);
// Skip pools that fail to load
}
}
setPoolDetails(details);
} catch (err) {
console.error('Error calling getAllPools:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch pools');
} finally {
setLoading(false);
}
};
fetchPools();
}, [publicClient, mounted, offset, limit]);
return {
pools,
poolDetails,
loading,
error,
isReady: mounted,
};
}
export interface SwapMintAmounts {
amountInUsed: bigint;
fee: bigint;
lpMinted: bigint;
calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5%
}
export interface BurnSwapAmounts {
amountOut: bigint;
outFee: bigint;
calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5%
}
export function useSwapMintAmounts(
poolAddress: `0x${string}` | undefined,
inputTokenIndex: number | undefined,
maxAmountIn: bigint | undefined,
inputTokenDecimals: number | undefined // Decimals of the input token
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
const [swapMintAmounts, setSwapMintAmounts] = useState<SwapMintAmounts | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Handle hydration for Next.js static export
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted || !poolAddress || inputTokenIndex === undefined || !maxAmountIn || maxAmountIn === BigInt(0)) {
setLoading(false);
setSwapMintAmounts(null);
return;
}
const fetchSwapMintAmounts = async () => {
if (!publicClient) return;
// Early validation
if (inputTokenDecimals === undefined) {
setError('inputTokenDecimals is required but was undefined');
return;
}
try {
setLoading(true);
setError(null);
const chainId = await publicClient.getChainId();
const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!partyInfoAddress) {
setError('PartyInfo contract not found for current chain');
setSwapMintAmounts(null);
return;
}
const result = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyPoolViewerABI,
functionName: 'swapMintAmounts',
args: [poolAddress, BigInt(inputTokenIndex), maxAmountIn],
}) 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({
amountInUsed: result[0],
fee: result[2],
lpMinted: result[1],
calculatedSlippage,
});
} catch (err) {
console.error('Error calling swapMintAmounts:', err);
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch swap mint amounts';
setError(errorMessage);
setSwapMintAmounts(null);
} finally {
setLoading(false);
}
};
fetchSwapMintAmounts();
}, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn, inputTokenDecimals]);
return {
swapMintAmounts,
loading,
error,
isReady: mounted,
};
}
export function useBurnSwapAmounts(
poolAddress: `0x${string}` | undefined,
lpAmount: bigint | undefined,
inputTokenIndex: number | undefined,
tokenDecimals: number | undefined // Decimals of the output token
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
const [burnSwapAmounts, setBurnSwapAmounts] = useState<BurnSwapAmounts | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Handle hydration for Next.js static export
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted || !poolAddress || !lpAmount || lpAmount === BigInt(0) || inputTokenIndex === undefined) {
setLoading(false);
setBurnSwapAmounts(null);
return;
}
const fetchBurnSwapAmounts = async () => {
if (!publicClient) {
setLoading(false);
return;
}
try {
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) {
setError('PartyInfo contract not found for current chain');
setBurnSwapAmounts(null);
return;
}
// Call burnSwapAmounts function - returns [amountOut, outFee]
const result = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyPoolViewerABI,
functionName: 'burnSwapAmounts',
args: [poolAddress, lpAmount, BigInt(inputTokenIndex)],
}) as readonly [bigint, bigint];
// Calculate slippage for burnSwap using poolPrice
let calculatedSlippage: number | undefined;
if (tokenDecimals !== undefined) {
try {
// 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 = {
amountOut: result[0],
outFee: result[1],
calculatedSlippage,
};
setBurnSwapAmounts(parsedAmounts);
} catch (err) {
console.error('Error calling burnSwapAmounts:', err);
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch burn swap amounts';
setError(errorMessage);
setBurnSwapAmounts(null);
} finally {
setLoading(false);
}
};
fetchBurnSwapAmounts();
}, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex, tokenDecimals]);
return {
burnSwapAmounts,
loading,
error,
isReady: mounted,
};
}
export function useLPTokenBalance(
poolAddress: `0x${string}` | undefined,
userAddress: `0x${string}` | undefined
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
const [lpBalance, setLpBalance] = useState<bigint | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Handle hydration for Next.js static export
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted || !poolAddress || !userAddress) {
setLoading(false);
setLpBalance(null);
return;
}
const fetchLPBalance = async () => {
if (!publicClient) {
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Call balanceOf on the pool (which is an ERC20 LP token)
const balance = await publicClient.readContract({
address: poolAddress,
abi: ERC20ABI,
functionName: 'balanceOf',
args: [userAddress],
}) as bigint;
setLpBalance(balance);
} catch (err) {
console.error('Error fetching LP token balance:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch LP balance');
setLpBalance(null);
} finally {
setLoading(false);
}
};
fetchLPBalance();
}, [publicClient, mounted, poolAddress, userAddress]);
return {
lpBalance,
loading,
error,
isReady: mounted,
};
}
export function useBurnAmounts(
poolAddress: `0x${string}` | undefined,
lpTokenAmount: bigint | undefined
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
const [burnAmounts, setBurnAmounts] = useState<bigint[] | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted || !poolAddress || !lpTokenAmount || lpTokenAmount === 0n) {
setBurnAmounts(null);
setLoading(false);
return;
}
const fetchBurnAmounts = async () => {
if (!publicClient) {
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Get chain ID and PartyInfo contract address
const chainId = await publicClient.getChainId();
const partyInfoAddress = (chainInfo as Record<string, { v1: { PartyInfo: string } }>)[chainId.toString()]?.v1?.PartyInfo;
if (!partyInfoAddress) {
setError('PartyInfo contract not found for current chain');
setBurnAmounts(null);
return;
}
// Call burnAmounts function
const amounts = await publicClient.readContract({
address: partyInfoAddress as `0x${string}`,
abi: IPartyPoolViewerABI,
functionName: 'burnAmounts',
args: [poolAddress, lpTokenAmount],
}) as bigint[];
setBurnAmounts(amounts);
} catch (err) {
console.error('Error fetching burn amounts:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch burn amounts');
setBurnAmounts(null);
} finally {
setLoading(false);
}
};
fetchBurnAmounts();
}, [publicClient, mounted, poolAddress, lpTokenAmount]);
return {
burnAmounts,
loading,
error,
isReady: mounted,
};
}

897
src/hooks/usePartyPool.ts Normal file
View File

@@ -0,0 +1,897 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { usePublicClient, useWalletClient } from 'wagmi';
import { decodeEventLog, parseUnits } from 'viem';
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
import chainInfo from '../contracts/liqp-deployments.json';
import IPartyInfoABI from '@/contracts/IPartyInfoABI';
import type { AvailableToken } from './usePartyPlanner';
// Q96 constant for price calculations
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 {
tokenAddress: `0x${string}`;
tokenSymbol: string;
amountIn: bigint;
amountOut: bigint;
fee: bigint;
poolAddress: `0x${string}`;
kappa: bigint;
inputTokenIndex: number;
outputTokenIndex: number;
calculatedSlippage?: number; // Percentage, e.g. 5.5 means 5.5%
}
/**
* Selects the best swap route from an array of routes
* Primary criterion: lowest fee
* Secondary criterion: highest kappa (if fees are equal)
*/
export function selectBestSwapRoute(routes: SwapAmountResult[]): SwapAmountResult | null {
console.log('selectBestSwapRoute called with', routes.length, 'routes');
if (routes.length === 0) {
console.log('No routes available');
return null;
}
console.log('All routes:', routes.map(r => ({
token: r.tokenSymbol,
pool: r.poolAddress,
fee: r.fee.toString(),
kappa: r.kappa.toString(),
amountOut: r.amountOut.toString(),
})));
const bestRoute = routes.reduce((best, current) => {
// Primary: lowest fee
if (current.fee < best.fee) return current;
if (current.fee > best.fee) return best;
// Secondary: if fees are equal, highest kappa
if (current.kappa > best.kappa) return current;
return best;
});
console.log('Selected best route:', {
token: bestRoute.tokenSymbol,
pool: bestRoute.poolAddress,
fee: bestRoute.fee.toString(),
kappa: bestRoute.kappa.toString(),
amountOut: bestRoute.amountOut.toString(),
});
return bestRoute;
}
export function useSwapAmounts(
availableTokens: AvailableToken[] | null,
fromAmount: string,
fromTokenDecimals: number,
slippagePercent: number
) {
const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false);
const [swapAmounts, setSwapAmounts] = useState<SwapAmountResult[] | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted || !availableTokens || !fromAmount || parseFloat(fromAmount) <= 0) {
setSwapAmounts(null);
setLoading(false);
return;
}
const calculateSwapAmounts = async () => {
if (!publicClient) {
setLoading(false);
return;
}
try {
setLoading(true);
const amountInTokenUnits = parseUnits(fromAmount, fromTokenDecimals);
const results: SwapAmountResult[] = [];
const chainId = await publicClient.getChainId();
const partyInfoAddress = (chainInfo as any)[chainId]?.v1?.PartyInfo as `0x${string}` | undefined;
// Calculate swap amounts for ALL routes of each token
for (const token of availableTokens) {
if (token.swapRoutes.length === 0) continue;
const routeResults: SwapAmountResult[] = [];
// Evaluate ALL routes for this token
for (const route of token.swapRoutes) {
try {
// Get swap amounts with NO 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 kappa for this pool
const kappa = await publicClient.readContract({
address: route.poolAddress,
abi: IPartyPoolABI,
functionName: 'kappa',
}) as bigint;
// Calculate slippage for this route
let calculatedSlippage: number | undefined;
if (partyInfoAddress) {
try {
// Get the current market price from PoolInfo
const priceInt128 = await publicClient.readContract({
address: partyInfoAddress,
abi: IPartyInfoABI,
functionName: 'price',
args: [route.poolAddress, BigInt(route.inputTokenIndex), BigInt(route.outputTokenIndex)],
}) as bigint;
// 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, swapOutputAmountDecimal, swapInputAmountDecimal, swapFeeDecimal);
console.log('calculatedSlippage', calculatedSlippage)
} catch (slippageErr) {
console.error(`Error calculating slippage for ${token.symbol}:`, slippageErr);
}
}
routeResults.push({
tokenAddress: token.address,
tokenSymbol: token.symbol,
amountIn: swapInputAmount,
amountOut: swapOutputAmount,
fee: inFee,
poolAddress: route.poolAddress,
kappa,
inputTokenIndex: route.inputTokenIndex,
outputTokenIndex: route.outputTokenIndex,
calculatedSlippage,
});
} catch (err) {
console.error(`Error calculating swap for ${token.symbol} via ${route.poolAddress}:`, err);
}
}
// Select the best route for this token using the shared helper
const bestRoute = selectBestSwapRoute(routeResults);
if (bestRoute) {
results.push(bestRoute);
}
}
setSwapAmounts(results);
} catch (err) {
console.error('Error calculating swap amounts:', err);
} finally {
setLoading(false);
}
};
calculateSwapAmounts();
}, [publicClient, mounted, availableTokens, fromAmount, fromTokenDecimals, slippagePercent]);
return {
swapAmounts,
loading,
};
}
export interface GasEstimate {
totalGas: bigint;
gasPrice: bigint;
estimatedCostEth: string;
estimatedCostUsd: string;
}
export interface ActualSwapAmounts {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
}
export interface ActualSwapMintAmounts {
amountInUsed: bigint;
lpMinted: bigint;
lpFee: bigint;
protocolFee: bigint;
}
export interface ActualBurnSwapAmounts {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
}
export function useSwap() {
const { data: walletClient } = useWalletClient();
const publicClient = usePublicClient();
const [isSwapping, setIsSwapping] = useState(false);
const [swapHash, setSwapHash] = useState<`0x${string}` | null>(null);
const [swapError, setSwapError] = useState<string | null>(null);
const [gasEstimate, setGasEstimate] = useState<GasEstimate | null>(null);
const [isEstimatingGas, setIsEstimatingGas] = useState(false);
const estimateGas = useCallback(async () => {
if (!publicClient) {
console.error('Public client not available');
return null;
}
try {
setIsEstimatingGas(true);
// Get current gas price from the network
const gasPrice = await publicClient.getGasPrice();
// Use fixed, typical gas amounts for AMM operations
const approvalGas = 50000n; // ERC20 approval typically uses ~50k gas
const swapGas = 150000n; // AMM swap typically uses ~150k gas
const totalGas = approvalGas + swapGas;
const estimatedCostWei = totalGas * gasPrice;
// Use more decimal places for testnets with very low gas prices
const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(9);
// Fetch ETH price in USD
let estimatedCostUsd = '0.00';
try {
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
const data = await response.json();
const ethPriceUsd = data.ethereum?.usd || 0;
const costInUsd = parseFloat(estimatedCostEth) * ethPriceUsd;
// Show "< $0.01" for amounts less than a cent, otherwise show 2 decimal places
estimatedCostUsd = costInUsd < 0.01 && costInUsd > 0 ? '< $0.01' : costInUsd.toFixed(2);
} catch (priceErr) {
console.error('Error fetching ETH price:', priceErr);
}
const estimate: GasEstimate = {
totalGas,
gasPrice,
estimatedCostEth,
estimatedCostUsd,
};
setGasEstimate(estimate);
return estimate;
} catch (err) {
console.error('Error estimating gas:', err);
return null;
} finally {
setIsEstimatingGas(false);
}
}, [publicClient]);
const executeSwap = async (
poolAddress: `0x${string}`,
inputTokenAddress: `0x${string}`,
inputTokenIndex: number,
outputTokenIndex: number,
maxAmountIn: bigint,
slippagePercent: number
) => {
if (!walletClient || !publicClient) {
setSwapError('Wallet not connected');
return;
}
try {
setIsSwapping(true);
setSwapError(null);
setSwapHash(null);
const userAddress = walletClient.account.address;
// STEP 1: Approve the pool to spend the input token
console.log('🔐 Approving token spend...');
console.log('Token to approve:', inputTokenAddress);
console.log('Spender (pool):', poolAddress);
console.log('Amount:', maxAmountIn.toString());
const approvalHash = await walletClient.writeContract({
address: inputTokenAddress,
abi: [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }]
}
],
functionName: 'approve',
args: [poolAddress, maxAmountIn],
});
console.log('✅ Approval transaction submitted:', approvalHash);
await publicClient.waitForTransactionReceipt({ hash: approvalHash });
console.log('✅ Approval confirmed');
// STEP 2: Calculate deadline
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200);
console.log('🚀 Executing swap with params:', {
pool: poolAddress,
payer: userAddress,
receiver: userAddress,
inputTokenIndex,
outputTokenIndex,
maxAmountIn: maxAmountIn.toString(),
limitPrice: '0 (no limit)',
deadline: deadline.toString(),
unwrap: false,
});
// STEP 3: Execute the swap transaction with no limit price
const hash = await walletClient.writeContract({
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'swap',
args: [
userAddress, // payer
'0x00000000', // selector (bytes4(0))
userAddress, // receiver
BigInt(inputTokenIndex),
BigInt(outputTokenIndex),
maxAmountIn,
0n, // no limit price
deadline,
false, // unwrap
'0x', // cbData (empty bytes)
],
});
setSwapHash(hash);
console.log('✅ Swap transaction submitted:', hash);
// Wait for transaction confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('✅ Swap transaction confirmed:', receipt);
// Parse the Swap event from the receipt logs
let actualSwapAmounts: ActualSwapAmounts | null = null;
for (const log of receipt.logs) {
try {
const decodedLog = decodeEventLog({
abi: IPartyPoolABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'Swap') {
const { amountIn, amountOut, lpFee, protocolFee } = decodedLog.args as {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
};
actualSwapAmounts = {
amountIn,
amountOut,
lpFee,
protocolFee,
};
console.log('📊 Actual swap amounts from event:', {
amountIn: amountIn.toString(),
amountOut: amountOut.toString(),
lpFee: lpFee.toString(),
protocolFee: protocolFee.toString(),
});
break;
}
} catch (err) {
// Skip logs that don't match our ABI
continue;
}
}
return { receipt, actualSwapAmounts };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Swap failed';
setSwapError(errorMessage);
console.error('❌ Swap error:', err);
throw err;
} finally {
setIsSwapping(false);
}
};
return {
executeSwap,
estimateGas,
isSwapping,
swapHash,
swapError,
gasEstimate,
isEstimatingGas,
};
}
export function useSwapMint() {
const publicClient = usePublicClient();
const { data: walletClient } = useWalletClient();
const [isSwapMinting, setIsSwapMinting] = useState(false);
const [swapMintHash, setSwapMintHash] = useState<`0x${string}` | null>(null);
const [swapMintError, setSwapMintError] = useState<string | null>(null);
const executeSwapMint = async (
poolAddress: `0x${string}`,
inputTokenAddress: `0x${string}`,
inputTokenIndex: number,
maxAmountIn: bigint
) => {
if (!walletClient || !publicClient) {
setSwapMintError('Wallet not connected');
return;
}
try {
setIsSwapMinting(true);
setSwapMintError(null);
setSwapMintHash(null);
const userAddress = walletClient.account.address;
// STEP 1: Approve the pool to spend the input token
console.log('🔐 Approving token spend for swap mint...');
console.log('Token to approve:', inputTokenAddress);
console.log('Spender (pool):', poolAddress);
console.log('Amount:', maxAmountIn.toString());
const approvalHash = await walletClient.writeContract({
address: inputTokenAddress,
abi: [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }]
}
],
functionName: 'approve',
args: [poolAddress, maxAmountIn],
});
console.log('✅ Approval transaction submitted:', approvalHash);
await publicClient.waitForTransactionReceipt({ hash: approvalHash });
console.log('✅ Approval confirmed');
// STEP 2: Calculate deadline (5 minutes from now)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 300); // 5 minutes = 300 seconds
console.log('🚀 Executing swapMint with params:', {
pool: poolAddress,
payer: userAddress,
receiver: userAddress,
inputTokenIndex,
maxAmountIn: maxAmountIn.toString(),
deadline: deadline.toString(),
});
// STEP 3: Execute the swapMint transaction
const hash = await walletClient.writeContract({
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'swapMint',
args: [
userAddress, // payer
userAddress, // receiver
BigInt(inputTokenIndex),
maxAmountIn,
deadline,
],
});
setSwapMintHash(hash);
console.log('✅ SwapMint transaction submitted:', hash);
// Wait for transaction confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('✅ SwapMint transaction confirmed:', receipt);
// Parse the SwapMint event from the receipt logs
let actualSwapMintAmounts: ActualSwapMintAmounts | null = null;
for (const log of receipt.logs) {
try {
const decodedLog = decodeEventLog({
abi: IPartyPoolABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'SwapMint') {
const { amountIn, amountOut, lpFee, protocolFee } = decodedLog.args as {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
};
actualSwapMintAmounts = {
amountInUsed: amountIn,
lpMinted: amountOut,
lpFee,
protocolFee,
};
console.log('📊 Actual swap mint amounts from event:', {
amountInUsed: amountIn.toString(),
lpMinted: amountOut.toString(),
lpFee: lpFee.toString(),
protocolFee: protocolFee.toString(),
});
break;
}
} catch (err) {
// Skip logs that don't match our ABI
continue;
}
}
return { receipt, actualSwapMintAmounts };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'SwapMint failed';
setSwapMintError(errorMessage);
console.error('❌ SwapMint error:', err);
throw err;
} finally {
setIsSwapMinting(false);
}
};
return {
executeSwapMint,
isSwapMinting,
swapMintHash,
swapMintError,
};
}
export function useBurnSwap() {
const publicClient = usePublicClient();
const { data: walletClient } = useWalletClient();
const [isBurnSwapping, setIsBurnSwapping] = useState(false);
const [burnSwapHash, setBurnSwapHash] = useState<`0x${string}` | null>(null);
const [burnSwapError, setBurnSwapError] = useState<string | null>(null);
const executeBurnSwap = async (
poolAddress: `0x${string}`,
lpAmount: bigint,
inputTokenIndex: number,
unwrap: boolean = false
) => {
if (!walletClient || !publicClient) {
setBurnSwapError('Wallet not connected');
return;
}
try {
setIsBurnSwapping(true);
setBurnSwapError(null);
setBurnSwapHash(null);
const userAddress = walletClient.account.address;
// STEP 1: Approve the pool to spend the LP tokens
console.log('🔐 Approving LP token spend for burn swap...');
console.log('LP token (pool) to approve:', poolAddress);
console.log('Spender (pool):', poolAddress);
console.log('Amount:', lpAmount.toString());
const approvalHash = await walletClient.writeContract({
address: poolAddress,
abi: [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }]
}
],
functionName: 'approve',
args: [poolAddress, lpAmount],
});
console.log('✅ Approval transaction submitted:', approvalHash);
await publicClient.waitForTransactionReceipt({ hash: approvalHash });
console.log('✅ Approval confirmed');
// STEP 2: Calculate deadline (5 minutes from now)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 300); // 5 minutes = 300 seconds
console.log('🚀 Executing burnSwap with params:', {
pool: poolAddress,
payer: userAddress,
receiver: userAddress,
lpAmount: lpAmount.toString(),
inputTokenIndex,
deadline: deadline.toString(),
unwrap,
});
// STEP 3: Execute the burnSwap transaction
const hash = await walletClient.writeContract({
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'burnSwap',
args: [
userAddress, // payer
userAddress, // receiver
lpAmount,
BigInt(inputTokenIndex),
deadline,
unwrap,
],
});
setBurnSwapHash(hash);
console.log('✅ BurnSwap transaction submitted:', hash);
// Wait for transaction confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('✅ BurnSwap transaction confirmed:', receipt);
// Parse the BurnSwap event from the receipt logs
let actualBurnSwapAmounts: ActualBurnSwapAmounts | null = null;
for (const log of receipt.logs) {
try {
const decodedLog = decodeEventLog({
abi: IPartyPoolABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'BurnSwap') {
const { amountIn, amountOut, lpFee, protocolFee } = decodedLog.args as {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
};
actualBurnSwapAmounts = {
amountIn,
amountOut,
lpFee,
protocolFee,
};
console.log('📊 Actual burn swap amounts from event:', {
amountIn: amountIn.toString(),
amountOut: amountOut.toString(),
lpFee: lpFee.toString(),
protocolFee: protocolFee.toString(),
});
break;
}
} catch (err) {
// Skip logs that don't match our ABI
continue;
}
}
return { receipt, actualBurnSwapAmounts };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'BurnSwap failed';
setBurnSwapError(errorMessage);
console.error('❌ BurnSwap error:', err);
throw err;
} finally {
setIsBurnSwapping(false);
}
};
return {
executeBurnSwap,
isBurnSwapping,
burnSwapHash,
burnSwapError,
};
}
export interface ActualBurnAmounts {
lpBurned: bigint;
withdrawAmounts: bigint[];
}
export function useBurn() {
const publicClient = usePublicClient();
const { data: walletClient } = useWalletClient();
const [isBurning, setIsBurning] = useState(false);
const [burnHash, setBurnHash] = useState<`0x${string}` | null>(null);
const [burnError, setBurnError] = useState<string | null>(null);
const executeBurn = async (
poolAddress: `0x${string}`,
lpAmount: bigint,
unwrap: boolean = false
) => {
if (!walletClient || !publicClient) {
setBurnError('Wallet not connected');
return;
}
try {
setIsBurning(true);
setBurnError(null);
setBurnHash(null);
const userAddress = walletClient.account.address;
// STEP 1: Approve the pool to spend the LP tokens
console.log('🔐 Approving LP token spend for burn...');
console.log('LP token (pool) to approve:', poolAddress);
console.log('Spender (pool):', poolAddress);
console.log('Amount:', lpAmount.toString());
const approvalHash = await walletClient.writeContract({
address: poolAddress,
abi: [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }]
}
],
functionName: 'approve',
args: [poolAddress, lpAmount],
account: userAddress,
});
console.log('✅ Approval transaction submitted:', approvalHash);
await publicClient.waitForTransactionReceipt({ hash: approvalHash });
console.log('✅ Approval confirmed');
// STEP 2: Calculate deadline (20 minutes from now)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200); // 20 minutes = 1200 seconds
console.log('🚀 Executing burn with params:', {
pool: poolAddress,
payer: userAddress,
receiver: userAddress,
lpAmount: lpAmount.toString(),
deadline: deadline.toString(),
unwrap,
});
// STEP 3: Execute the burn transaction
const hash = await walletClient.writeContract({
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'burn',
args: [
userAddress, // payer
userAddress, // receiver
lpAmount,
deadline,
unwrap,
],
account: userAddress,
});
setBurnHash(hash);
console.log('✅ Burn transaction submitted:', hash);
// Wait for transaction confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('✅ Burn transaction confirmed:', receipt);
// Parse the Burn event from the receipt logs
let actualBurnAmounts: ActualBurnAmounts | null = null;
for (const log of receipt.logs) {
try {
const decodedLog = decodeEventLog({
abi: IPartyPoolABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'Burn') {
// @ts-ignore
const { amounts, lpBurned } = decodedLog.args as {
amounts: bigint[];
lpBurned: bigint;
};
actualBurnAmounts = {
lpBurned,
withdrawAmounts: amounts,
};
console.log('📊 Actual burn amounts from event:', {
lpBurned: lpBurned.toString(),
withdrawAmounts: amounts.map(a => a.toString()),
});
break;
}
} catch (err) {
// Skip logs that don't match our ABI
continue;
}
}
return { receipt, actualBurnAmounts };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Burn failed';
setBurnError(errorMessage);
console.error('❌ Burn error:', err);
throw err;
} finally {
setIsBurning(false);
}
};
return {
executeBurn,
isBurning,
burnHash,
burnError,
};
}

404
src/init_pools.js Normal file
View File

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

View File

@@ -8,11 +8,11 @@
"stake": "Staken"
},
"swap": {
"title": "Tauschen",
"title": "",
"from": "Von",
"to": "Zu",
"youPay": "Sie zahlen",
"youReceive": "Sie erhalten",
"youPay": "Verkaufen",
"youReceive": "Kaufen",
"balance": "Guthaben",
"selectToken": "Token auswählen",
"swapButton": "Tauschen",

View File

@@ -8,11 +8,11 @@
"stake": "Stake"
},
"swap": {
"title": "Swap",
"title": "",
"from": "From",
"to": "To",
"youPay": "You pay",
"youReceive": "You receive",
"youPay": "Sell",
"youReceive": "Buy",
"balance": "Balance",
"selectToken": "Select token",
"swapButton": "Swap",
@@ -20,6 +20,15 @@
},
"stake": {
"title": "Stake",
"comingSoon": "Coming soon..."
"comingSoon": "Coming soon...",
"selectPool": "Select Pool",
"selectToken": "Select Token",
"amount": "Amount",
"stakeButton": "Stake",
"insufficientBalance": "Insufficient balance",
"amountUsed": "Amount Used",
"fee": "Fee",
"lpMinted": "LP Minted",
"amountOut": "Amount Out"
}
}

View File

@@ -8,11 +8,11 @@
"stake": "Apostar"
},
"swap": {
"title": "Intercambiar",
"title": "",
"from": "Desde",
"to": "Hasta",
"youPay": "Pagas",
"youReceive": "Recibes",
"youPay": "Vender",
"youReceive": "Comprar",
"balance": "Saldo",
"selectToken": "Seleccionar token",
"swapButton": "Intercambiar",

View File

@@ -8,11 +8,11 @@
"stake": "Staker"
},
"swap": {
"title": "Échanger",
"title": "",
"from": "De",
"to": "À",
"youPay": "Vous payez",
"youReceive": "Vous recevez",
"youPay": "Vendre",
"youReceive": "Acheter",
"balance": "Solde",
"selectToken": "Sélectionner un token",
"swapButton": "Échanger",

View File

@@ -8,11 +8,11 @@
"stake": "ステーキング"
},
"swap": {
"title": "スワップ",
"title": "",
"from": "から",
"to": "へ",
"youPay": "支払い",
"youReceive": "受取り",
"youPay": "売却",
"youReceive": "購入",
"balance": "残高",
"selectToken": "トークンを選択",
"swapButton": "スワップ",

View File

@@ -8,11 +8,11 @@
"stake": "质押"
},
"swap": {
"title": "兑换",
"title": "",
"from": "从",
"to": "到",
"youPay": "您支付",
"youReceive": "您收到",
"youPay": "卖出",
"youReceive": "买入",
"balance": "余额",
"selectToken": "选择代币",
"swapButton": "兑换",

View File

@@ -2,7 +2,7 @@
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { type ThemeProviderProps } from 'next-themes/dist/types';
import { type ThemeProviderProps } from 'next-themes';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;