Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions widget/NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ from https://app.uniswap.org/explore/pools/ethereum/0xdce6394339af00981949f5f3ba
Wallet address with Uniswap v4 LP positions:
Has 1 position:
0xb2e3e82a95f5c4c47e30a5b420ac4f99d32ef61f
0x6b97C363b5Ee55c7F83ab0d06Ff68cb459c643c6
0x835d41a4F2EF7d7B56178cFB01d6a1C6A3B623f2
Has 2 positions:
0xbA85a470abAB9A283E7EfeB7BfA30ae04f6067fA
Large account:
0xae2Fc483527B8EF99EB5D9B44875F005ba1FaE13


To Do:
- show position's range (might need to get from external API) Ask Charlie about if there's any websites he saw?
- show current price of pair
- show if price is in range or out
- alert user if out of range (future can adjust position automatically)

To generalize later:
- PoolID's link works for 1 chain (mainnet Ethereum), need to switch RPC
- PoolID's link works for 1 chain (mainnet Ethereum), need to switch RPC
- bug: unable to calculate price from ticks for small tokens
(e.g. dsync, Pool ID: 0x3f4b40bbbb1b6f8cdd48281c56b9ea7c7934715c735dda1c430ab4d156b96630)
100 changes: 96 additions & 4 deletions widget/components/example/PositionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { useEffect, useState } from 'react';
import { ethers } from 'ethers';
import { Token, Ether } from '@uniswap/sdk-core';
import { Token, Ether, Price } from '@uniswap/sdk-core';
import { tickToPrice } from '@uniswap/v4-sdk';
import { ProtocolPosition } from '@/types/portfolio';
import { getPoolId } from '../utils/getPoolId';

Expand All @@ -18,12 +19,17 @@ const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL || 'https://rpc.ankr.com/eth';

// Updated ABI to match the packed return data for info
const ABI = [
'function getPoolAndPositionInfo(uint256 tokenId) external view returns (tuple(address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, bytes32 positionInfo)'
'function getPoolAndPositionInfo(uint256 tokenId) external view returns (tuple(address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, bytes32 positionInfo)',
'function getSlot0(bytes32 id) external view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)'
];

export default function PositionCard({ position, index, chainKey, chainId }: PositionCardProps) {
const [poolId, setPoolId] = useState<string>('Loading...');
const [ticks, setTicks] = useState<{ lower: number; upper: number } | null>(null);
const [prices, setPrices] = useState<{ lower: string; upper: string } | null>(null);
const [currentPrice, setCurrentPrice] = useState<string | null>(null);
const [currentTick, setCurrentTick] = useState<number | null>(null);
const [inRange, setInRange] = useState<boolean | null>(null);

useEffect(() => {
async function fetchPositionData() {
Expand All @@ -44,6 +50,11 @@ export default function PositionCard({ position, index, chainKey, chainId }: Pos
const result = await contract.getPoolAndPositionInfo(tokenId);
const poolKey = result.poolKey;
const positionInfo = ethers.BigNumber.from(result.positionInfo);
console.log('Debug PositionInfo:', {
hex: positionInfo.toHexString(),
tickLower24: positionInfo.shr(128).mask(24).toNumber(),
tickUpper24: positionInfo.shr(152).mask(24).toNumber()
});

// Unpack PositionInfo
// Layout: liquidity (128 bits), tickLower (24 bits), tickUpper (24 bits)
Expand All @@ -64,6 +75,8 @@ export default function PositionCard({ position, index, chainKey, chainId }: Pos
const tickLower = parseTick(tickLower24);
const tickUpper = parseTick(tickUpper24);

console.log('Debug Ticks:', { tickLower, tickUpper });

if (v4Position.assets && v4Position.assets.length >= 2 && chainId) {
const getCurrency = (address: string, asset: any) => {
if (address === '0x0000000000000000000000000000000000000000') {
Expand All @@ -81,15 +94,62 @@ export default function PositionCard({ position, index, chainKey, chainId }: Pos
const token0 = getCurrency(poolKey.currency0, v4Position.assets[0]);
const token1 = getCurrency(poolKey.currency1, v4Position.assets[1]);

console.log('Debug Pool Data:', {
token0: { address: token0.isNative ? 'NATIVE' : token0.address, decimals: token0.decimals, symbol: token0.symbol },
token1: { address: token1.isNative ? 'NATIVE' : token1.address, decimals: token1.decimals, symbol: token1.symbol },
fee: poolKey.fee,
tickSpacing: poolKey.tickSpacing,
hooks: poolKey.hooks
});

const calculatedPoolId = getPoolId(
token0,
token1,
poolKey.fee,
poolKey.tickSpacing,
Number(poolKey.fee),
Number(poolKey.tickSpacing),
poolKey.hooks
);
console.log('Debug Calculated PoolID:', calculatedPoolId);

setPoolId(calculatedPoolId);
setTicks({ lower: tickLower, upper: tickUpper });

// Calculate Price Range
let priceLowerVal = 0;
let priceUpperVal = 0;
try {
const priceLower = tickToPrice(token0, token1, tickLower);
const priceUpper = tickToPrice(token0, token1, tickUpper);
priceLowerVal = parseFloat(priceLower.toSignificant(6));
priceUpperVal = parseFloat(priceUpper.toSignificant(6));

setPrices({
lower: priceLower.toSignificant(6),
upper: priceUpper.toSignificant(6)
});
} catch (priceError) {
console.warn('Failed to calculate price range:', priceError);
setPrices(null);
}

// Calculate Current Price from Assets
if (v4Position.assets && v4Position.assets.length >= 2) {
const price0 = parseFloat(v4Position.assets[0].price);
const price1 = parseFloat(v4Position.assets[1].price);

if (price1 > 0) {
const currentPriceVal = price0 / price1;
setCurrentPrice(currentPriceVal.toPrecision(6));

// Calculate current tick
// tick = floor(log(price) / log(1.0001))
const currentTickVal = Math.floor(Math.log(currentPriceVal) / Math.log(1.0001));
setCurrentTick(currentTickVal);

// Check if in range using ticks
setInRange(currentTickVal >= tickLower && currentTickVal < tickUpper);
}
}
}
} catch (error) {
console.error('Error fetching position data:', error);
Expand Down Expand Up @@ -132,6 +192,38 @@ export default function PositionCard({ position, index, chainKey, chainId }: Pos
Tick range: ({ticks.lower}, {ticks.upper})
</p>
)}
{prices && (
<p className="text-xs text-gray-500">
Price range: ({prices.lower}, {prices.upper}) {position.assets?.[1].symbol} per {position.assets?.[0].symbol}
</p>
)}
{currentPrice && (
<div className="text-xs text-gray-500">
<p>Current Price: {currentPrice} {position.assets?.[1].symbol} per {position.assets?.[0].symbol}</p>

{currentTick !== null && ticks && (
<div className="mt-2">
<div className="relative h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={`absolute top-0 bottom-0 w-2 rounded-full ${inRange ? 'bg-green-500' : 'bg-red-500'}`}
style={{
left: `${Math.max(0, Math.min(100, ((currentTick - ticks.lower) / (ticks.upper - ticks.lower)) * 100))}%`,
transform: 'translateX(-50%)'
}}
/>
</div>
<div className="flex justify-between text-[10px] text-gray-400 mt-1">
<span>{prices?.lower}</span>
<span>{prices?.upper}</span>
</div>
</div>
)}

<p className={`mt-1 ${inRange ? "text-green-600 font-medium" : "text-red-600 font-medium"}`}>
{inRange ? "✅ In Range" : "🚨 Out of Range"}
</p>
</div>
)}
</div>
);
}
Loading