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:
- Create the component files in your projectās
components/uniswap
directory - 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.