├── .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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------