├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── next.svg └── vercel.svg ├── screenshot.png ├── src ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── orderbooks │ │ ├── uniswap-v2.tsx │ │ └── uniswap-v3.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── switch.tsx │ │ └── table.tsx └── lib │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alternate defi 2 | 3 | Orderbook view of amms with rpc or subgraph across chains. 4 | 5 | 6 | # How to setup 7 | 8 | 9 | ## Prerequisites 10 | 11 | Before you begin, make sure you have Node.js (version 10.13 or later) and npm/yarn installed on your machine. 12 | 13 | 1. **Node.js**: [Download & Install Node.js](https://nodejs.org/en/download/) 14 | 2. **npm** comes with Node.js, but you can ensure it's updated by running `npm install npm@latest -g`. 15 | 3. **Yarn** (Optional): [Install Yarn](https://yarnpkg.com/getting-started/install) (if you prefer using Yarn over npm). 16 | 17 | 18 | 19 | ## Install 20 | 21 | 1. `git clone git@github.com:wellimbharath/alternate-defi.git` 22 | 2. `npm install` 23 | 3. `npm run dev` 24 | 25 | 26 | ## How to use 27 | 28 | 1. Get your subgraph Uniswap V3 Subgraph from 29 | - [Alchmey](https://subgraphs.alchemy.com/subgraphs/5603) or 30 | - [The Ghost ](https://thegraph.com/explorer/subgraphs/5zvR82QoaXYFyDEKLZ9t6v9adgnptxYpKpSbxtgVENFV?view=Query&chain=arbitrum-one) 31 | 32 | 2. Get your api key from 33 | - [Alchemy Subgraph](https://subgraphs.alchemy.com/subgraphs/5603) / 34 | - [The graph studio](https://thegraph.com/studio/) 35 | 36 | Add the `api-key` and `uniswap v3` liquidity pool to the site and visualize the orderbook: 37 | 38 | ![https://alternate-defi.vercel.app](screenshot.png) 39 | 40 | 41 | ❤️ Hope you liked it. Bye bye! -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alternate-defi", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-label": "^2.1.0", 13 | "@radix-ui/react-select": "^2.1.1", 14 | "@radix-ui/react-slot": "^1.1.0", 15 | "@radix-ui/react-switch": "^1.1.0", 16 | "@thanpolas/univ3prices": "^3.0.2", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.1", 19 | "ethers": "^5.7.2", 20 | "jsbi": "^4.3.0", 21 | "lucide-react": "^0.427.0", 22 | "next": "14.2.5", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "recharts": "^2.12.7", 26 | "tailwind-merge": "^2.4.0", 27 | "tailwindcss-animate": "^1.0.7" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^20", 31 | "@types/react": "^18", 32 | "@types/react-dom": "^18", 33 | "eslint": "^8", 34 | "eslint-config-next": "14.2.5", 35 | "postcss": "^8", 36 | "tailwindcss": "^3.4.1", 37 | "typescript": "^5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wellimbharath/alternate-defi/9e408915e73929fe4046207865b9f15e6a2a02c5/screenshot.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wellimbharath/alternate-defi/9e408915e73929fe4046207865b9f15e6a2a02c5/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 222.2 47.4% 11.2%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 222.2 84% 4.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 222.2 84% 4.9%; 36 | --foreground: 210 40% 98%; 37 | --card: 222.2 84% 4.9%; 38 | --card-foreground: 210 40% 98%; 39 | --popover: 222.2 84% 4.9%; 40 | --popover-foreground: 210 40% 98%; 41 | --primary: 210 40% 98%; 42 | --primary-foreground: 222.2 47.4% 11.2%; 43 | --secondary: 217.2 32.6% 17.5%; 44 | --secondary-foreground: 210 40% 98%; 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | --accent: 217.2 32.6% 17.5%; 48 | --accent-foreground: 210 40% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 217.2 32.6% 17.5%; 52 | --input: 217.2 32.6% 17.5%; 53 | --ring: 212.7 26.8% 83.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Alternate Defi", 9 | description: "An orderbook view of the AMMs", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import UniswapV3Orderbook from "@/components/orderbooks/uniswap-v3"; 2 | import Head from "next/head"; 3 | import Image from "next/image"; 4 | 5 | 6 | export default function Home() { 7 | return ( 8 | <> 9 | 10 | Alternate Defi 11 | 12 | 13 |
14 | 15 |
16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/orderbooks/uniswap-v2.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wellimbharath/alternate-defi/9e408915e73929fe4046207865b9f15e6a2a02c5/src/components/orderbooks/uniswap-v2.tsx -------------------------------------------------------------------------------- /src/components/orderbooks/uniswap-v3.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState, useCallback, useEffect, useMemo } from 'react'; 3 | import { ethers } from 'ethers'; 4 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; 5 | import { Input } from "@/components/ui/input"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 8 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 9 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 10 | import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'; 11 | import { Github, Loader2 } from 'lucide-react'; 12 | 13 | import { Label } from "@/components/ui/label"; 14 | import { Switch } from '../ui/switch'; 15 | const UNISWAP_V3_POOL_ABI = [ 16 | "function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)", 17 | "function liquidity() external view returns (uint128)", 18 | "function ticks(int24 tick) external view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized)", 19 | "function token0() external view returns (address)", 20 | "function token1() external view returns (address)", 21 | "function fee() external view returns (uint24)", 22 | "function tickSpacing() external view returns (int24)" 23 | ]; 24 | 25 | const ERC20_ABI = [ 26 | "function decimals() external view returns (uint8)", 27 | "function symbol() external view returns (string)" 28 | ]; 29 | 30 | interface OrderbookEntry { 31 | price: number; 32 | liquidity: string; 33 | type: string; 34 | tickIdx: number; 35 | } 36 | 37 | interface TokenPair { 38 | name: string; 39 | address: string; 40 | } 41 | 42 | 43 | const Q96 = ethers.BigNumber.from(2).pow(96); 44 | const Q192 = ethers.BigNumber.from(2).pow(192); 45 | 46 | const tickToPrice = (tick: number): number => { 47 | return 1.0001 ** tick; 48 | } 49 | 50 | const priceToSqrtP = (price: number): ethers.BigNumber => { 51 | if (price <= 0) return ethers.constants.Zero; 52 | return ethers.BigNumber.from( 53 | ethers.utils.parseUnits(Math.sqrt(price).toFixed(8), 8) 54 | ).mul(Q96).div(ethers.utils.parseUnits('1', 8)); 55 | }; 56 | 57 | const sqrtPToPrice = (sqrtP: ethers.BigNumber): ethers.BigNumber => { 58 | if (sqrtP.isZero()) return ethers.BigNumber.from(0); 59 | 60 | const price = sqrtP.mul(sqrtP).div(Q192); 61 | return price; 62 | }; 63 | 64 | const getLiquidityForAmount0 = (sqrtA: ethers.BigNumber, sqrtB: ethers.BigNumber, amount0: ethers.BigNumber): ethers.BigNumber => { 65 | if (sqrtA.gt(sqrtB)) [sqrtA, sqrtB] = [sqrtB, sqrtA]; 66 | if (sqrtA.isZero() || sqrtB.isZero()) return ethers.constants.Zero; 67 | const numerator = amount0.mul(sqrtA).mul(sqrtB).div(Q96); 68 | const denominator = sqrtB.sub(sqrtA); 69 | return denominator.isZero() ? ethers.constants.Zero : numerator.div(denominator); 70 | }; 71 | 72 | const getLiquidityForAmount1 = (sqrtA: ethers.BigNumber, sqrtB: ethers.BigNumber, amount1: ethers.BigNumber): ethers.BigNumber => { 73 | if (sqrtA.gt(sqrtB)) [sqrtA, sqrtB] = [sqrtB, sqrtA]; 74 | if (sqrtA.isZero() || sqrtB.isZero()) return ethers.constants.Zero; 75 | const denominator = sqrtB.sub(sqrtA); 76 | return denominator.isZero() ? ethers.constants.Zero : amount1.mul(Q96).div(denominator); 77 | }; 78 | 79 | const getAmount0ForLiquidity = (sqrtA: ethers.BigNumber, sqrtB: ethers.BigNumber, liquidity: ethers.BigNumber): ethers.BigNumber => { 80 | if (sqrtA.gt(sqrtB)) [sqrtA, sqrtB] = [sqrtB, sqrtA]; 81 | if (sqrtA.isZero() || sqrtB.isZero() || liquidity.isZero()) return ethers.constants.Zero; 82 | return liquidity.mul(Q96).mul(sqrtB.sub(sqrtA)).div(sqrtA).div(sqrtB); 83 | }; 84 | 85 | const getAmount1ForLiquidity = (sqrtA: ethers.BigNumber, sqrtB: ethers.BigNumber, liquidity: ethers.BigNumber): ethers.BigNumber => { 86 | if (sqrtA.gt(sqrtB)) [sqrtA, sqrtB] = [sqrtB, sqrtA]; 87 | if (sqrtA.isZero() || sqrtB.isZero() || liquidity.isZero()) return ethers.constants.Zero; 88 | return liquidity.mul(sqrtB.sub(sqrtA)).div(Q96); 89 | }; 90 | 91 | 92 | const UniswapV3Orderbook: React.FC = () => { 93 | const [invertPrices, setInvertPrices] = useState(false); 94 | 95 | const [dataSource, setDataSource] = useState<'subgraph' | 'rpc'>('subgraph'); 96 | const [rpcUrl, setRpcUrl] = useState(''); 97 | const [subgraphUrl, setSubgraphUrl] = useState('https://subgraph.satsuma-prod.com/[api-key]/perosnal--524835/community/uniswap-v3-mainnet/version/0.0.1/api'); 98 | const [selectedPair, setSelectedPair] = useState(null); 99 | const [customContractAddress, setCustomContractAddress] = useState('0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640'); 100 | const [currentPrice, setCurrentPrice] = useState(null); 101 | const [orderbook, setOrderbook] = useState([]); 102 | const [error, setError] = useState(null); 103 | const [token0Symbol, setToken0Symbol] = useState(''); 104 | const [token0Decimals, setToken0Decimals] = useState(0); 105 | const [token1Decimals, setToken1Decimals] = useState(0); 106 | const [isLoading, setIsLoading] = useState(false); 107 | 108 | const [token1Symbol, setToken1Symbol] = useState(''); 109 | const [poolFee, setPoolFee] = useState(null); 110 | 111 | const fetchSubgraphData = useCallback(async () => { 112 | const query = ` 113 | query ($poolAddress: ID!, $skip: Int!) { 114 | pool(id: $poolAddress) { 115 | token0 { 116 | symbol 117 | decimals 118 | } 119 | token1 { 120 | symbol 121 | decimals 122 | } 123 | feeTier 124 | sqrtPrice 125 | tick 126 | ticks(first: 1000, skip: $skip, orderBy: tickIdx) { 127 | tickIdx 128 | liquidityNet 129 | price0 130 | price1 131 | } 132 | } 133 | } 134 | `; 135 | 136 | let allTicks: any = []; 137 | let skip = 0; 138 | let poolData = null; 139 | 140 | while (true) { 141 | const response = await fetch(subgraphUrl, { 142 | method: 'POST', 143 | headers: { 'Content-Type': 'application/json' }, 144 | body: JSON.stringify({ 145 | query, 146 | variables: { 147 | poolAddress: customContractAddress.toLowerCase(), 148 | skip: skip 149 | }, 150 | }), 151 | }); 152 | 153 | const result = await response.json(); 154 | if (result.errors) throw new Error(result.errors[0].message); 155 | 156 | const currentData = result.data.pool; 157 | 158 | if (!poolData) { 159 | poolData = currentData; 160 | } 161 | 162 | allTicks = allTicks.concat(currentData.ticks); 163 | 164 | if (currentData.ticks.length < 1000) { 165 | break; 166 | } 167 | 168 | skip += 1000; 169 | } 170 | 171 | poolData.ticks = allTicks; 172 | return poolData; 173 | }, [subgraphUrl, customContractAddress]); 174 | 175 | 176 | const fetchRpcData = useCallback(async () => { 177 | const provider = new ethers.providers.JsonRpcProvider(rpcUrl); 178 | const poolContract = new ethers.Contract(customContractAddress, UNISWAP_V3_POOL_ABI, provider); 179 | 180 | const [token0Address, token1Address, slot0Data, tickSpacing, fee] = await Promise.all([ 181 | poolContract.token0(), 182 | poolContract.token1(), 183 | poolContract.slot0(), 184 | poolContract.tickSpacing(), 185 | poolContract.fee(), 186 | ]); 187 | 188 | const token0Contract = new ethers.Contract(token0Address, ERC20_ABI, provider); 189 | const token1Contract = new ethers.Contract(token1Address, ERC20_ABI, provider); 190 | 191 | const [token0Symbol, token1Symbol, token0Decimals, token1Decimals] = await Promise.all([ 192 | token0Contract.symbol(), 193 | token1Contract.symbol(), 194 | token0Contract.decimals(), 195 | token1Contract.decimals(), 196 | ]); 197 | 198 | const currentTick = slot0Data.tick; 199 | const ticks = []; 200 | for (let i = -50; i <= 50; i++) { 201 | const tickIdx = Math.round(currentTick / tickSpacing) * tickSpacing + i * tickSpacing; 202 | const tickData = await poolContract.ticks(tickIdx); 203 | ticks.push({ 204 | tickIdx, 205 | liquidityNet: tickData.liquidityNet.toString(), 206 | price0: (1.0001 ** tickIdx).toString(), 207 | price1: (1 / 1.0001 ** tickIdx).toString(), 208 | }); 209 | } 210 | 211 | return { 212 | token0: { symbol: token0Symbol, decimals: token0Decimals }, 213 | token1: { symbol: token1Symbol, decimals: token1Decimals }, 214 | feeTier: fee.toString(), 215 | sqrtPrice: slot0Data.sqrtPriceX96.toString(), 216 | tick: currentTick.toString(), 217 | ticks, 218 | }; 219 | }, [rpcUrl, customContractAddress]); 220 | 221 | const processPoolData = useCallback((poolData: any) => { 222 | 223 | setToken0Symbol(poolData.token0.symbol); 224 | setToken1Symbol(poolData.token1.symbol); 225 | 226 | setToken0Decimals(parseInt(poolData.token0.decimals)); 227 | setToken1Decimals(parseInt(poolData.token1.decimals)); 228 | 229 | setPoolFee(parseFloat(poolData.feeTier) / 10000); 230 | 231 | let token0Decimals = parseInt(poolData.token0.decimals); 232 | let token1Decimals = parseInt(poolData.token1.decimals); 233 | 234 | const currentTick = parseInt(poolData.tick); 235 | const sqrtPrice = ethers.BigNumber.from(poolData.sqrtPrice); 236 | 237 | let currentPrice = (tickToPrice(currentTick) * (10 ** (token0Decimals - token1Decimals))); 238 | 239 | setCurrentPrice(currentPrice); 240 | 241 | const asks: OrderbookEntry[] = []; 242 | const bids: OrderbookEntry[] = []; 243 | 244 | let cumulativeLiquidity = ethers.BigNumber.from(0); 245 | 246 | poolData.ticks.forEach((tick: any) => { 247 | const tickIdx = parseInt(tick.tickIdx); 248 | const liquidityNet = ethers.BigNumber.from(tick.liquidityNet); 249 | cumulativeLiquidity = cumulativeLiquidity.add(liquidityNet); 250 | 251 | if (cumulativeLiquidity.gt(0)) { 252 | const sqrtPriceX96 = priceToSqrtP(1.0001 ** tickIdx); 253 | 254 | let price = (tickToPrice(tickIdx) * (10 ** (token0Decimals - token1Decimals))); 255 | 256 | // Calculate amounts based on liquidity 257 | const amount0 = getAmount0ForLiquidity(sqrtPrice, sqrtPriceX96, cumulativeLiquidity); 258 | const amount1 = getAmount1ForLiquidity(sqrtPrice, sqrtPriceX96, cumulativeLiquidity); 259 | 260 | 261 | const liquidity0 = getLiquidityForAmount0(sqrtPrice, sqrtPriceX96, amount0); 262 | const liquidity1 = getLiquidityForAmount1(sqrtPrice, sqrtPriceX96, amount1); 263 | 264 | // Convert amounts to decimal representation 265 | const decimalAmount0 = parseFloat(ethers.utils.formatUnits(liquidity0, token0Decimals)); 266 | const decimalAmount1 = parseFloat(ethers.utils.formatUnits(liquidity1, token1Decimals)); 267 | 268 | 269 | if (decimalAmount0 > 0 && decimalAmount1 > 0) { 270 | 271 | const entry: OrderbookEntry = { 272 | price : price, 273 | liquidity: decimalAmount1.toFixed(token1Decimals).toString(), 274 | type: tickIdx > currentTick ? 'bid' : 'ask', 275 | tickIdx: tickIdx 276 | }; 277 | 278 | 279 | if (tickIdx > currentTick) { 280 | if (entry.price > 0) { 281 | bids.push(entry); 282 | } 283 | } else { 284 | if (entry.price > 0) { 285 | asks.push(entry); 286 | } 287 | } 288 | } 289 | } 290 | }); 291 | 292 | // Sort asks from lowest to highest price 293 | asks.sort((a, b) => b.tickIdx - a.tickIdx); 294 | // Sort bids from highest to lowest price 295 | bids.sort((a, b) => a.tickIdx - b.tickIdx); 296 | 297 | // Slice to get top 50 asks and bids 298 | const topAsks = asks.slice(0, 15); 299 | const topBids = bids.slice(0, 15); 300 | 301 | // Combine and set the orderbook 302 | setOrderbook([...topAsks.reverse(), ...topBids]); 303 | }, []); 304 | 305 | const fetchPoolData = async () => { 306 | setError(null); 307 | setIsLoading(true); 308 | try { 309 | if (dataSource === 'subgraph' && !subgraphUrl) { 310 | throw new Error('Subgraph URL is required for subgraph data source'); 311 | } 312 | if (dataSource === 'rpc' && !rpcUrl) { 313 | throw new Error('RPC URL is required for RPC data source'); 314 | } 315 | const contractAddress = selectedPair ? selectedPair.address : customContractAddress; 316 | if (!contractAddress) { 317 | throw new Error('Contract address is required'); 318 | } 319 | const poolData = dataSource === 'subgraph' ? await fetchSubgraphData() : await fetchRpcData(); 320 | processPoolData(poolData); 321 | } catch (err) { 322 | console.error(err); 323 | setError(`Failed to fetch pool data: ${err instanceof Error ? err.message : 'Unknown error'}`); 324 | } finally { 325 | setIsLoading(false); 326 | } 327 | }; 328 | 329 | const chartData = useMemo(() => { 330 | let bidSum = 0; 331 | let askSum = 0; 332 | return orderbook.map(order => { 333 | if (order.type === 'bid') { 334 | bidSum += parseFloat(order.liquidity); 335 | return { price: order.price, bidDepth: bidSum, askDepth: 0 }; 336 | } else { 337 | askSum += parseFloat(order.liquidity); 338 | return { price: order.price, bidDepth: 0, askDepth: askSum }; 339 | } 340 | }); 341 | }, [orderbook]); 342 | 343 | const CustomTooltip = ({ active, payload, label }: any) => { 344 | if (active && payload && payload.length) { 345 | return ( 346 |
347 |

Price: {label.toFixed(6)}

348 | {payload[0].value > 0 && ( 349 |

Bid Depth: {payload[0].value.toFixed(2)}

350 | )} 351 | {payload[1].value > 0 && ( 352 |

Ask Depth: {payload[1].value.toFixed(2)}

353 | )} 354 |
355 | ); 356 | } 357 | return null; 358 | }; 359 | const handleInvertPrices = () => { 360 | setInvertPrices(!invertPrices); 361 | }; 362 | return ( 363 | 364 | <> 365 | 366 | Uniswap v3 Orderbook 367 |
368 | 377 | {dataSource === 'subgraph' ? ( 378 | <> 379 | setSubgraphUrl(e.target.value)} 382 | placeholder="Enter Subgraph URL" 383 | /> 384 | 385 | * get your api key from Alchemy or TheGraph 386 | 387 | ) : ( 388 | setRpcUrl(e.target.value)} 391 | placeholder="Enter RPC URL" 392 | /> 393 | 394 | )} 395 | {/* */} 405 |
406 | {/* or */} 407 | <> 408 | 409 | setCustomContractAddress(e.target.value)} 412 | placeholder="Enter custom pool contract address" 413 | /> 414 | * Uni v3 Pool Address 415 | 416 |
417 | 425 |
426 | {error && ( 427 | 428 | Error 429 | {error} 430 | 431 | )} 432 | 433 |
434 | 435 | 436 |
437 | {currentPrice !== null && ( 438 | <> 439 |
440 |

Token Pair: {token0Symbol} / {token1Symbol}

441 | 442 |
443 |

Pool Fee: {poolFee}%

444 |

Current Price: {invertPrices ? (1 / currentPrice).toFixed(token1Decimals) : currentPrice.toFixed(token0Decimals)} {invertPrices ? `${token0Symbol}/${token1Symbol}` : `${token1Symbol}/${token0Symbol}`}

445 | 446 |
447 | 448 | 449 | 450 | value.toFixed(2)} 455 | /> 456 | 457 | } /> 458 | 465 | 472 | 473 | 474 | 475 |
476 | 477 | 478 | 479 | 480 | )} 481 | 489 | 490 |
491 | 492 | 493 | 494 | 495 |
496 | 497 | 498 | 499 | Price ({invertPrices ? token0Symbol : token1Symbol}) 500 | 501 | Liquidity ({token1Symbol}) 502 | Type 503 | Tick Idx 504 | 505 | 506 | 507 | {orderbook.map((order, index) => ( 508 | 509 | {invertPrices ? (1 / order.price).toFixed(token0Decimals) : order.price.toFixed(token1Decimals)} 510 | {parseFloat(order.liquidity).toFixed(2)} 511 | {order.type.toUpperCase()} 512 | {order.tickIdx} 513 | 514 | ))} 515 | 516 |
517 |
518 |
519 | 520 | 521 |
522 | Made with ❤️ by @wellimbharath 523 |
524 | 525 |
526 | 527 | 537 | 538 |
539 | ); 540 | }; 541 | 542 | export default UniswapV3Orderbook; -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1", 23 | className 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )) 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )) 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )) 100 | SelectContent.displayName = SelectPrimitive.Content.displayName 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )) 135 | SelectItem.displayName = SelectPrimitive.Item.displayName 136 | 137 | const SelectSeparator = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef 140 | >(({ className, ...props }, ref) => ( 141 | 146 | )) 147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 148 | 149 | export { 150 | Select, 151 | SelectGroup, 152 | SelectValue, 153 | SelectTrigger, 154 | SelectContent, 155 | SelectLabel, 156 | SelectItem, 157 | SelectSeparator, 158 | SelectScrollUpButton, 159 | SelectScrollDownButton, 160 | } 161 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | 93 | )) 94 | TableCell.displayName = "TableCell" 95 | 96 | const TableCaption = React.forwardRef< 97 | HTMLTableCaptionElement, 98 | React.HTMLAttributes 99 | >(({ className, ...props }, ref) => ( 100 |
105 | )) 106 | TableCaption.displayName = "TableCaption" 107 | 108 | export { 109 | Table, 110 | TableHeader, 111 | TableBody, 112 | TableFooter, 113 | TableHead, 114 | TableRow, 115 | TableCell, 116 | TableCaption, 117 | } 118 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [require("tailwindcss-animate")], 78 | } satisfies Config 79 | 80 | export default config -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "checkJs": false, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------