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
6 changes: 5 additions & 1 deletion widget/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# Your Octav API KEY
OCTAV_API_KEY=""
OCTAV_API_KEY=""

# RPC URL for mainnet Ethereum
# (used for fetching data from Uniswap v4 Position Manager contract)
NEXT_PUBLIC_RPC_URL=""
20 changes: 20 additions & 0 deletions widget/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Wallet has Uniswap v4 LP position
from https://app.uniswap.org/explore/pools/ethereum/0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d


Wallet address with Uniswap v4 LP positions:
Has 1 position:
0xb2e3e82a95f5c4c47e30a5b420ac4f99d32ef61f
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
16 changes: 9 additions & 7 deletions widget/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Portfolio from '@/components/example/portfolio';
import Uniswap4PositionsClient from '@/components/example/uniswap4-positions-client';
import Uniswap4PositionsWidget from '@/components/example/uniswap4-positions-widget';
import { generateMetadata } from '@/lib/metadata';

export const metadata = generateMetadata({
Expand All @@ -8,15 +8,17 @@ export const metadata = generateMetadata({
});

export default function Page() {
const ownerAddress = '0x6426af179aabebe47666f345d69fd9079673f6cd';
// const ownerAddress = '0x6426af179aabebe47666f345d69fd9079673f6cd';

// Account with Uniswap v4 Positions: 0xbA85a470abAB9A283E7EfeB7BfA30ae04f6067fA
const ownerAddress = '0xbA85a470abAB9A283E7EfeB7BfA30ae04f6067fA';

return (
<div className="flex flex-col gap-4">
<p className="py-4 mt-2 md:mt-0">Hello</p>
<p>Example widget</p>
<Portfolio />
<p>Uniswap v4 LP positions widget</p>
<Uniswap4PositionsClient initialAddress={ownerAddress} />
{/* <p>Example widget</p>
<Portfolio ownerAddress={ownerAddress} /> */}
<h1 className="text-2xl font-bold">Uniswap v4 LP positions widget</h1>
<Uniswap4PositionsWidget initialAddress={ownerAddress} />
</div>
);
}
8 changes: 7 additions & 1 deletion widget/components/app-sidebar/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import * as React from 'react';
import { Home } from 'lucide-react';
import { Home, Laptop } from 'lucide-react';

import { NavItem, NavItems } from '@/components/app-sidebar/nav-items';

Expand All @@ -21,6 +21,12 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
pathname: '/',
icon: <Home width={20} height={20} className="stroke-[1.5]" />,
},
{
name: 'Create Position',
url: 'http://localhost:3000',
pathname: '/',
icon: <Laptop width={20} height={20} className="stroke-[1.5]" />,
},
];

return (
Expand Down
137 changes: 137 additions & 0 deletions widget/components/example/PositionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
'use client';

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

interface PositionCardProps {
position: ProtocolPosition;
index: number;
chainKey: string;
chainId: number;
}

const POSITION_MANAGER_ADDRESS = '0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e';
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)'
];

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);

useEffect(() => {
async function fetchPositionData() {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const v4Position = position as any;
// Assuming the position name contains the tokenId, e.g., "#123"
const tokenId = v4Position.name.replace('#', '');

if (!tokenId || isNaN(Number(tokenId))) {
setPoolId('Invalid Token ID');
return;
}

const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const contract = new ethers.Contract(POSITION_MANAGER_ADDRESS, ABI, provider);

const result = await contract.getPoolAndPositionInfo(tokenId);
const poolKey = result.poolKey;
const positionInfo = ethers.BigNumber.from(result.positionInfo);

// Unpack PositionInfo
// Layout: liquidity (128 bits), tickLower (24 bits), tickUpper (24 bits)
// Note: This layout is an assumption based on common packing.
// We might need to adjust if the values look wrong.

const tickLower24 = positionInfo.shr(128).mask(24).toNumber();
const tickUpper24 = positionInfo.shr(152).mask(24).toNumber();

// Convert to signed 24-bit integers
const parseTick = (tick: number) => {
if (tick & 0x800000) {
return tick - 0x1000000;
}
return tick;
};

const tickLower = parseTick(tickLower24);
const tickUpper = parseTick(tickUpper24);

if (v4Position.assets && v4Position.assets.length >= 2 && chainId) {
const getCurrency = (address: string, asset: any) => {
if (address === '0x0000000000000000000000000000000000000000') {
return Ether.onChain(chainId);
}
return new Token(
chainId,
address,
parseInt(asset.decimal),
asset.symbol,
asset.name
);
};

const token0 = getCurrency(poolKey.currency0, v4Position.assets[0]);
const token1 = getCurrency(poolKey.currency1, v4Position.assets[1]);

const calculatedPoolId = getPoolId(
token0,
token1,
poolKey.fee,
poolKey.tickSpacing,
poolKey.hooks
);
setPoolId(calculatedPoolId);
setTicks({ lower: tickLower, upper: tickUpper });
}
} catch (error) {
console.error('Error fetching position data:', error);
setPoolId('Error');
}
}

fetchPositionData();
}, [position, chainId]);

return (
<div className="p-2 border border-gray-200 rounded-md bg-white">
<p className="font-medium text-sm">Position {index + 1}: {position.assets?.[0].symbol}/{position.assets?.[1].symbol}</p>
<p className="text-xs text-gray-500">Value: ${position.value}</p>

<p className="font-medium text-sm truncate">
NFT:{" "}
<a
href={`https://opensea.io/item/${chainKey}/${position.poolAddress}/${position.name.replace('#', '')}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>{`${position.poolAddress}/${position.name.replace('#', '')}`}</a>
</p>

<p className="text-xs text-gray-500">
Pool ID:{" "}
<a
href={`https://app.uniswap.org/explore/pools/${chainKey}/${poolId}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{poolId}
</a>
</p>

{ticks && (
<p className="text-xs text-gray-500">
Tick range: ({ticks.lower}, {ticks.upper})
</p>
)}
</div>
);
}
14 changes: 12 additions & 2 deletions widget/components/example/portfolio.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use client';

import { useGetPortfolio } from '@/services/octav/loader';
// import { getPoolId } from '../utils/getPoolId';
// import { ChainId, Token, WETH9 } from '@uniswap/sdk-core';

export default function Portfolio() {
export default function Portfolio({ ownerAddress = '0x6426af179aabebe47666f345d69fd9079673f6cd' }) {
const { data, isLoading, error } = useGetPortfolio({
address: '0x6426af179aabebe47666f345d69fd9079673f6cd',
// address: '0x6426af179aabebe47666f345d69fd9079673f6cd',
address: ownerAddress,
includeImages: true,
includeExplorerUrls: true,
waitForSync: true,
Expand All @@ -22,10 +25,17 @@ export default function Portfolio() {
}
console.log(data);

// const chainId = ChainId.MAINNET;
// const USDC = new Token(chainId, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD Coin');
// const WETH = WETH9[chainId];

// const poolIdExample = getPoolId(WETH, USDC, 3000, 60, '0x0000000000000000000000000000000000000000');

return (
<div className="p-4 border border-gray-300 bg-gray-50 rounded-md">
<p className="font-semibold text-gray-800">Net Worth for {data?.address}</p>
<p className="text-gray-600">${data?.networth}</p>
{/* <p className="text-gray-600">Example Pool ID: {poolIdExample}</p> */}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import { useState } from 'react';
import Uniswap4Positions from './uniswap4-positions';
import Portfolio from './portfolio';

type Props = {
initialAddress?: string;
};

export default function Uniswap4PositionsClient({ initialAddress = '0x6426af179aabebe47666f345d69fd9079673f6cd' }: Props) {
export default function Uniswap4PositionsWidget({ initialAddress = '0xbA85a470abAB9A283E7EfeB7BfA30ae04f6067fA' }: Props) {
const [ownerAddress, setOwnerAddress] = useState(initialAddress);

return (
Expand All @@ -22,6 +23,7 @@ export default function Uniswap4PositionsClient({ initialAddress = '0x6426af179a
/>
</label>

<Portfolio ownerAddress={ownerAddress} />
<Uniswap4Positions ownerAddress={ownerAddress} />
</div>
);
Expand Down
56 changes: 47 additions & 9 deletions widget/components/example/uniswap4-positions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use client';

import { ProtocolPosition } from '@/types/portfolio';
import { useGetPortfolio } from '@/services/octav/loader';

import PositionCard from './PositionCard';

interface Uniswap4PositionsProps {
ownerAddress?: string;
}
Expand All @@ -25,23 +28,58 @@ export default function Uniswap4Positions({ ownerAddress = '0x6426af179aabebe476
);
}
console.log(data);
// Wallet address with Uniswap v4 LP positions:
// Smaller account:
// 0xbA85a470abAB9A283E7EfeB7BfA30ae04f6067fA
// Large account:
// 0xae2Fc483527B8EF99EB5D9B44875F005ba1FaE13

const uniswapValue = data?.assetByProtocols?.uniswap4?.value;
const numOfPositions = data?.assetByProtocols?.uniswap4?.chains.ethereum?.protocolPositions.LIQUIDITYPOOL.protocolPositions?.length ?? 0;
const positions = data?.assetByProtocols?.uniswap4?.chains.ethereum?.protocolPositions.LIQUIDITYPOOL.protocolPositions;

const chains = data?.assetByProtocols?.uniswap4?.chains;

const renderPositionsByChain = () => {
if (!chains) {
return null;
}

return Object.keys(chains).map((chainKey) => {
const chainData = chains[chainKey];
const positions = chainData.protocolPositions?.LIQUIDITYPOOL?.protocolPositions;
const numOfPositions = chainData.protocolPositions?.LIQUIDITYPOOL?.protocolPositions?.length;

if (!positions || positions.length === 0) {
return null;
}

return (
<div key={chainKey} className="mt-4">
<h3 className="text-md font-semibold capitalize mb-2">{chainKey}</h3>
<p className="text-gray-600">Number of positions: {numOfPositions ?? 0}</p>
<div className="space-y-2">
{positions.map((position: ProtocolPosition, index: number) => {
const chainId = Number(data?.chains?.[chainKey]?.chainId);
return (
<PositionCard
key={index}
position={position}
index={index}
chainKey={chainKey}
chainId={chainId}
/>
);
})}
</div>
</div>
);
});
};

return (
<div className="p-4 border border-gray-300 bg-gray-50 rounded-md">
<p className="font-semibold text-gray-800">Uniswap v4 LP positions for {data?.address}</p>
<p className="font-semibold text-gray-800">Uniswap v4 LP positions</p>
{uniswapValue != null ? (
<>
<p className="text-gray-600">Total value: ${uniswapValue}</p>
<p className="text-gray-600">Number of positions: {numOfPositions}</p>
{renderPositionsByChain()}
</>
) : null}
) : <p className="text-gray-600">No Uniswap v4 positions found.</p>}
</div>
);
}
13 changes: 13 additions & 0 deletions widget/components/utils/getPoolId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Pool } from '@uniswap/v4-sdk';
import { Currency } from '@uniswap/sdk-core';

export function getPoolId(
currency0: Currency,
currency1: Currency,
fee: number,
tickSpacing: number,
hooks: string
): string {
const poolId = Pool.getPoolId(currency0, currency1, fee, tickSpacing, hooks);
return poolId;
}
6 changes: 3 additions & 3 deletions widget/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.74.4",
"@uniswap/sdk-core": "^7.9.0",
"@uniswap/v4-sdk": "^1.23.0",
"axios": "^1.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"ethers": "^6.15.0",
"ethers": "5.7.2",
"lucide-react": "^0.503.0",
"next": "16.0.3",
"react": "^19.2.0",
"react-day-picker": "8.10.1",
"react-dom": "^19.2.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
Expand Down
Loading
Loading