Skip to Content
Advanced Mini CoursesUniswap IntroductionInteractive Examples and Visualizations

Interactive Examples and Visualizations

Understanding Uniswap’s mechanics is easier with interactive examples and visualizations. In this section, we’ll provide code for interactive components that you can integrate into your Next.js application with Tailwind CSS and ShadCN UI.

https://www.desmos.com/calculator/7wbvkts2jf 

//SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "../lib/openzeppelin-contracts-5/contracts/token/ERC20/IERC20.sol"; import "../lib/openzeppelin-contracts-5/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "../lib/openzeppelin-contracts-5/contracts/access/Ownable.sol"; import "../lib/openzeppelin-contracts-5/contracts/utils/ReentrancyGuard.sol"; // Universal Router imports import "../lib/universal-router/contracts/interfaces/IUniversalRouter.sol"; import "../lib/universal-router/contracts/libraries/Commands.sol"; import "../lib/universal-router/permit2/src/interfaces/IAllowanceTransfer.sol"; /** * @title MorpherSwapHelper * @dev Helper contract to simplify swapping with Universal Router by requiring only one permit */ contract MorpherSwapHelper is Ownable, ReentrancyGuard { // Universal Router and Permit2 addresses address public immutable universalRouter; address public immutable permit2; // Events event SwapExecuted( address indexed user, address inputToken, address outputToken, uint256 amountIn, uint256 amountOut ); /** * @dev Constructor sets the Universal Router and Permit2 addresses * @param _universalRouter Address of the Universal Router * @param _permit2 Address of the Permit2 contract */ constructor(address _universalRouter, address _permit2) Ownable(msg.sender) { universalRouter = _universalRouter; permit2 = _permit2; } /** * @dev Execute a swap with a single permit * @param userAddr Address of the user who owns the tokens and will receive the output * @param inputToken Address of the input token * @param outputToken Address of the output token * @param amountIn Amount of input tokens to swap * @param amountOutMin Minimum amount of output tokens expected * @param path Encoded path for the swap * @param deadline Deadline for the transaction * @param permitDeadline Deadline for the permit * @param v v component of the signature * @param r r component of the signature * @param s s component of the signature * @return amountOut Amount of output tokens received */ function swapWithPermit( address userAddr, address inputToken, address outputToken, uint256 amountIn, uint256 amountOutMin, bytes memory path, uint256 deadline, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s ) external nonReentrant returns (uint256 amountOut) { // 1. Use the permit to get approval for this contract to spend user's tokens IERC20Permit(inputToken).permit(userAddr, address(this), amountIn, permitDeadline, v, r, s); // 2. Transfer tokens from user to this contract IERC20(inputToken).transferFrom(userAddr, address(this), amountIn); // 3. Approve Permit2 to spend our tokens IERC20(inputToken).approve(permit2, amountIn); // 4. Approve Permit2 to Universal Router IAllowanceTransfer(permit2).approve(inputToken, universalRouter, uint160(amountIn), uint48(deadline)); uint256 balanceBefore = IERC20(outputToken).balanceOf(userAddr); { // 5. Prepare the swap inputs bytes[] memory inputs = new bytes[](1); // Encode the parameters for the V3_SWAP_EXACT_IN command inputs[0] = abi.encode( userAddr, // recipient (send tokens directly to user) amountIn, // amountIn amountOutMin, // amountOutMinimum path, // path true // payerIsUser - false because tokens come from this contract ); // 6. Record balance before swap to calculate output amount // 7. Execute the swap IUniversalRouter(universalRouter).execute( abi.encodePacked(uint8(Commands.V3_SWAP_EXACT_IN)), inputs, deadline ); } // 8. Calculate amount received amountOut = IERC20(outputToken).balanceOf(userAddr) - balanceBefore; // 9. Emit event emit SwapExecuted(userAddr, inputToken, outputToken, amountIn, amountOut); return amountOut; } /** * @dev Rescue any tokens accidentally sent to this contract * @param token Address of the token to rescue * @param to Address to send the tokens to * @param amount Amount of tokens to rescue */ function rescueTokens(address token, address to, uint256 amount) external onlyOwner { IERC20(token).transfer(to, amount); } }

1. SqrtPrice Calculator Component

This component allows users to convert between price and sqrtPriceX96:

// components/uniswap/SqrtPriceCalculator.tsx import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; export function SqrtPriceCalculator() { const [price, setPrice] = useState<string>('2000'); const [sqrtPriceX96, setSqrtPriceX96] = useState<string>(''); const [activeTab, setActiveTab] = useState<string>('price-to-sqrt'); // Convert price to sqrtPriceX96 const convertPriceToSqrtPriceX96 = (price: number): string => { if (isNaN(price) || price <= 0) return ''; // Calculate square root of price const sqrtPrice = Math.sqrt(price); // Scale by 2^96 const sqrtPriceX96 = sqrtPrice * Math.pow(2, 96); return sqrtPriceX96.toLocaleString('fullwide', { useGrouping: false }); }; // Convert sqrtPriceX96 to price const convertSqrtPriceX96ToPrice = (sqrtPriceX96: number): string => { if (isNaN(sqrtPriceX96) || sqrtPriceX96 <= 0) return ''; // Calculate price from sqrtPriceX96 const price = Math.pow(sqrtPriceX96 / Math.pow(2, 96), 2); return price.toString(); }; // Update sqrtPriceX96 when price changes useEffect(() => { if (activeTab === 'price-to-sqrt') { const numPrice = parseFloat(price); setSqrtPriceX96(convertPriceToSqrtPriceX96(numPrice)); } }, [price, activeTab]); // Update price when sqrtPriceX96 changes useEffect(() => { if (activeTab === 'sqrt-to-price') { const numSqrtPriceX96 = parseFloat(sqrtPriceX96); setPrice(convertSqrtPriceX96ToPrice(numSqrtPriceX96)); } }, [sqrtPriceX96, activeTab]); return ( <Card className="w-full max-w-md mx-auto"> <CardHeader> <CardTitle>SqrtPrice Calculator</CardTitle> <CardDescription> Convert between price and sqrtPriceX96 format used by Uniswap v3 </CardDescription> </CardHeader> <CardContent> <Tabs defaultValue="price-to-sqrt" onValueChange={setActiveTab}> <TabsList className="grid w-full grid-cols-2"> <TabsTrigger value="price-to-sqrt">Price → SqrtPriceX96</TabsTrigger> <TabsTrigger value="sqrt-to-price">SqrtPriceX96 → Price</TabsTrigger> </TabsList> <TabsContent value="price-to-sqrt" className="space-y-4"> <div className="space-y-2"> <Label htmlFor="price">Price</Label> <Input id="price" type="number" value={price} onChange={(e) => setPrice(e.target.value)} placeholder="Enter price (e.g., 2000)" /> </div> <div className="space-y-2"> <Label htmlFor="sqrtPriceX96">SqrtPriceX96</Label> <Input id="sqrtPriceX96" value={sqrtPriceX96} readOnly className="bg-muted" /> </div> </TabsContent> <TabsContent value="sqrt-to-price" className="space-y-4"> <div className="space-y-2"> <Label htmlFor="sqrtPriceX96-input">SqrtPriceX96</Label> <Input id="sqrtPriceX96-input" value={sqrtPriceX96} onChange={(e) => setSqrtPriceX96(e.target.value)} placeholder="Enter sqrtPriceX96" /> </div> <div className="space-y-2"> <Label htmlFor="price-output">Price</Label> <Input id="price-output" value={price} readOnly className="bg-muted" /> </div> </TabsContent> </Tabs> </CardContent> </Card> ); }

2. Tick Range Visualizer

This component visualizes liquidity concentration across tick ranges:

// components/uniswap/TickRangeVisualizer.tsx import React, { useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; export function TickRangeVisualizer() { const [currentPrice, setCurrentPrice] = useState<number>(2000); const [lowerBound, setLowerBound] = useState<number>(1500); const [upperBound, setUpperBound] = useState<number>(2500); // Calculate percentage of price range const priceRange = 5000; // Max price in visualization const lowerPercent = (lowerBound / priceRange) * 100; const currentPercent = (currentPrice / priceRange) * 100; const upperPercent = (upperBound / priceRange) * 100; // Calculate capital efficiency compared to v2 const v2Efficiency = 100; // baseline const v3Efficiency = v2Efficiency * (priceRange / (upperBound - lowerBound)); return ( <Card className="w-full max-w-2xl mx-auto"> <CardHeader> <CardTitle>Concentrated Liquidity Visualizer</CardTitle> <CardDescription> Visualize how concentrated liquidity works in Uniswap v3 </CardDescription> </CardHeader> <CardContent className="space-y-6"> <div className="space-y-2"> <Label>Current Price: ${currentPrice}</Label> <Slider value={[currentPrice]} min={100} max={5000} step={10} onValueChange={(value) => setCurrentPrice(value[0])} /> </div> <div className="space-y-2"> <Label>Lower Price Bound: ${lowerBound}</Label> <Slider value={[lowerBound]} min={100} max={currentPrice} step={10} onValueChange={(value) => setLowerBound(value[0])} /> </div> <div className="space-y-2"> <Label>Upper Price Bound: ${upperBound}</Label> <Slider value={[upperBound]} min={currentPrice} max={5000} step={10} onValueChange={(value) => setUpperBound(value[0])} /> </div> <div className="h-20 bg-muted rounded-md relative"> {/* Price range visualization */} <div className="absolute h-full bg-primary/20 rounded-md" style={{ left: `${lowerPercent}%`, width: `${upperPercent - lowerPercent}%` }} /> {/* Current price marker */} <div className="absolute h-full w-1 bg-primary rounded-full" style={{ left: `${currentPercent}%` }} /> {/* Labels */} <div className="absolute -bottom-6 text-xs" style={{ left: `${lowerPercent}%` }} > ${lowerBound} </div> <div className="absolute -bottom-6 text-xs font-bold" style={{ left: `${currentPercent}%` }} > ${currentPrice} </div> <div className="absolute -bottom-6 text-xs" style={{ left: `${upperPercent}%` }} > ${upperBound} </div> </div> <div className="p-4 bg-muted rounded-md"> <p className="text-sm"> <strong>Capital Efficiency:</strong> Your concentrated position is approximately{' '} <span className="text-primary font-bold"> {v3Efficiency.toFixed(0)}% </span>{' '} more efficient than a Uniswap v2 position with the same amount of capital. </p> </div> </CardContent> </Card> ); }

3. Swap Simulator

This component simulates swaps and shows price impact:

// components/uniswap/SwapSimulator.tsx import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { ArrowDownIcon } from "@radix-ui/react-icons"; export function SwapSimulator() { const [tokenAAmount, setTokenAAmount] = useState<string>('1'); const [tokenBAmount, setTokenBAmount] = useState<string>(''); const [poolTokenA, setPoolTokenA] = useState<number>(100); const [poolTokenB, setPoolTokenB] = useState<number>(200000); const [priceImpact, setPriceImpact] = useState<number>(0); // Calculate constant product const constantProduct = poolTokenA * poolTokenB; // Calculate token B amount based on token A input useEffect(() => { const tokenAInput = parseFloat(tokenAAmount); if (!isNaN(tokenAInput) && tokenAInput > 0) { // Calculate new pool balance after swap const newPoolTokenA = poolTokenA + tokenAInput; const newPoolTokenB = constantProduct / newPoolTokenA; // Calculate token B output const tokenBOutput = poolTokenB - newPoolTokenB; // Calculate price impact const initialPrice = poolTokenB / poolTokenA; const finalPrice = newPoolTokenB / newPoolTokenA; const impact = ((initialPrice - finalPrice) / initialPrice) * 100; setTokenBAmount(tokenBOutput.toFixed(2)); setPriceImpact(impact); } else { setTokenBAmount(''); setPriceImpact(0); } }, [tokenAAmount, poolTokenA, poolTokenB, constantProduct]); // Execute swap const executeSwap = () => { const tokenAInput = parseFloat(tokenAAmount); const tokenBOutput = parseFloat(tokenBAmount); if (!isNaN(tokenAInput) && !isNaN(tokenBOutput)) { setPoolTokenA(poolTokenA + tokenAInput); setPoolTokenB(poolTokenB - tokenBOutput); } }; return ( <Card className="w-full max-w-md mx-auto"> <CardHeader> <CardTitle>Swap Simulator</CardTitle> <CardDescription> Simulate swaps and see how they affect the pool </CardDescription> </CardHeader> <CardContent className="space-y-4"> <div className="p-4 bg-muted rounded-md space-y-2"> <p className="text-sm"> <strong>Pool Reserves:</strong> </p> <p className="text-sm"> ETH: {poolTokenA.toFixed(2)} </p> <p className="text-sm"> USDC: {poolTokenB.toFixed(2)} </p> <p className="text-sm"> <strong>Current Price:</strong> {(poolTokenB / poolTokenA).toFixed(2)} USDC per ETH </p> </div> <div className="space-y-2"> <Label htmlFor="tokenA">You Pay (ETH)</Label> <Input id="tokenA" type="number" value={tokenAAmount} onChange={(e) => setTokenAAmount(e.target.value)} placeholder="0.0" /> </div> <div className="flex justify-center"> <div className="bg-muted rounded-full p-2"> <ArrowDownIcon className="h-4 w-4" /> </div> </div> <div className="space-y-2"> <Label htmlFor="tokenB">You Receive (USDC)</Label> <Input id="tokenB" value={tokenBAmount} readOnly className="bg-muted" placeholder="0.0" /> </div> <div className="p-4 bg-muted rounded-md"> <p className="text-sm"> <strong>Price Impact:</strong>{' '} <span className={priceImpact > 5 ? 'text-destructive' : 'text-primary'}> {priceImpact.toFixed(2)}% </span> </p> </div> <Button className="w-full" onClick={executeSwap} disabled={!tokenAAmount || parseFloat(tokenAAmount) <= 0} > Swap </Button> </CardContent> </Card> ); }

4. Pool Initialization Simulator

This component simulates initializing a new pool:

// components/uniswap/PoolInitializer.tsx import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; export function PoolInitializer() { const [token0Symbol, setToken0Symbol] = useState<string>('WETH'); const [token1Symbol, setToken1Symbol] = useState<string>('USDC'); const [initialPrice, setInitialPrice] = useState<string>('2000'); const [feeTier, setFeeTier] = useState<string>('3000'); const [sqrtPriceX96, setSqrtPriceX96] = useState<string>(''); const [poolInitialized, setPoolInitialized] = useState<boolean>(false); // Calculate sqrtPriceX96 from initial price useEffect(() => { const price = parseFloat(initialPrice); if (!isNaN(price) && price > 0) { // Calculate square root of price const sqrtPrice = Math.sqrt(price); // Scale by 2^96 const sqrtPriceX96 = sqrtPrice * Math.pow(2, 96); setSqrtPriceX96(sqrtPriceX96.toLocaleString('fullwide', { useGrouping: false })); } else { setSqrtPriceX96(''); } }, [initialPrice]); // Initialize pool const initializePool = () => { setPoolInitialized(true); }; return ( <Card className="w-full max-w-md mx-auto"> <CardHeader> <CardTitle>Pool Initializer</CardTitle> <CardDescription> Simulate initializing a new Uniswap v3 pool </CardDescription> </CardHeader> <CardContent className="space-y-4"> <div className="grid grid-cols-2 gap-4"> <div className="space-y-2"> <Label htmlFor="token0">Token 0</Label> <Select value={token0Symbol} onValueChange={setToken0Symbol} disabled={poolInitialized} > <SelectTrigger id="token0"> <SelectValue placeholder="Select token" /> </SelectTrigger> <SelectContent> <SelectItem value="WETH">WETH</SelectItem> <SelectItem value="WBTC">WBTC</SelectItem> <SelectItem value="DAI">DAI</SelectItem> </SelectContent> </Select> </div> <div className="space-y-2"> <Label htmlFor="token1">Token 1</Label> <Select value={token1Symbol} onValueChange={setToken1Symbol} disabled={poolInitialized} > <SelectTrigger id="token1"> <SelectValue placeholder="Select token" /> </SelectTrigger> <SelectContent> <SelectItem value="USDC">USDC</SelectItem> <SelectItem value="USDT">USDT</SelectItem> <SelectItem value="DAI">DAI</SelectItem> </SelectContent> </Select> </div> </div> <div className="space-y-2"> <Label htmlFor="initialPrice">Initial Price ({token1Symbol} per {token0Symbol})</Label> <Input id="initialPrice" type="number" value={initialPrice} onChange={(e) => setInitialPrice(e.target.value)} placeholder="Enter initial price" disabled={poolInitialized} /> </div> <div className="space-y-2"> <Label htmlFor="feeTier">Fee Tier</Label> <Select value={feeTier} onValueChange={setFeeTier} disabled={poolInitialized} > <SelectTrigger id="feeTier"> <SelectValue placeholder="Select fee tier" /> </SelectTrigger> <SelectContent> <SelectItem value="500">0.05%</SelectItem> <SelectItem value="3000">0.3%</SelectItem> <SelectItem value="10000">1%</SelectItem> </SelectContent> </Select> </div> <div className="space-y-2"> <Label htmlFor="sqrtPriceX96">Calculated sqrtPriceX96</Label> <Input id="sqrtPriceX96" value={sqrtPriceX96} readOnly className="bg-muted text-xs" /> </div> {!poolInitialized ? ( <Button className="w-full" onClick={initializePool} disabled={!initialPrice || parseFloat(initialPrice) <= 0} > Initialize Pool </Button> ) : ( <div className="p-4 bg-green-100 dark:bg-green-900 rounded-md"> <p className="text-sm text-green-800 dark:text-green-200"> Pool initialized successfully! The {token0Symbol}/{token1Symbol} pool has been created with a {parseInt(feeTier)/10000}% fee and an initial price of {initialPrice} {token1Symbol} per {token0Symbol}. </p> <p className="text-sm text-green-800 dark:text-green-200 mt-2"> The pool is now ready to accept liquidity. </p> </div> )} </CardContent> </Card> ); }

How to Use These Components

To use these components in your Next.js application:

  1. Create the component files in your project’s components/uniswap directory
  2. Import and use them in your pages:
// pages/uniswap-examples.tsx import { SqrtPriceCalculator } from "@/components/uniswap/SqrtPriceCalculator"; import { TickRangeVisualizer } from "@/components/uniswap/TickRangeVisualizer"; import { SwapSimulator } from "@/components/uniswap/SwapSimulator"; import { PoolInitializer } from "@/components/uniswap/PoolInitializer"; export default function UniswapExamplesPage() { return ( <div className="container mx-auto py-10 space-y-10"> <h1 className="text-3xl font-bold">Uniswap Interactive Examples</h1> <section> <h2 className="text-2xl font-semibold mb-4">SqrtPrice Calculator</h2> <SqrtPriceCalculator /> </section> <section> <h2 className="text-2xl font-semibold mb-4">Concentrated Liquidity Visualizer</h2> <TickRangeVisualizer /> </section> <section> <h2 className="text-2xl font-semibold mb-4">Swap Simulator</h2> <SwapSimulator /> </section> <section> <h2 className="text-2xl font-semibold mb-4">Pool Initializer</h2> <PoolInitializer /> </section> </div> ); }

These interactive components will help users understand Uniswap’s concepts more intuitively. You can further enhance them with more detailed calculations, animations, or additional features as needed.

Last updated on