From 8f32a1a176add3e77a4ef16f59d2519e18f09849 Mon Sep 17 00:00:00 2001 From: mico Date: Mon, 2 Mar 2026 10:54:11 +0100 Subject: [PATCH 01/49] Initial redesign --- src/App.tsx | 1 + src/components/AllDropdown.tsx | 28 ++++ src/components/Header.tsx | 62 ++++---- src/components/LiquidityPoolRow.tsx | 102 +++++++------ src/components/LiquidityPoolsTable.tsx | 201 +++++++++++++------------ src/components/PositionCard.tsx | 87 +++++++++++ src/pages/LiquidityPools.tsx | 158 +++++++++++++++++-- 7 files changed, 455 insertions(+), 184 deletions(-) create mode 100644 src/components/AllDropdown.tsx create mode 100644 src/components/PositionCard.tsx diff --git a/src/App.tsx b/src/App.tsx index cb637ac..468d139 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,7 @@ function AppRouter() { } /> } /> + } /> } /> } /> diff --git a/src/components/AllDropdown.tsx b/src/components/AllDropdown.tsx new file mode 100644 index 0000000..b00bbee --- /dev/null +++ b/src/components/AllDropdown.tsx @@ -0,0 +1,28 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { ChevronDown } from 'lucide-react'; +import { Button } from './ui/button'; + +export function AllDropdown() { + return ( + + + + + + All + + + ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 3e932c0..162a50e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,43 +1,45 @@ -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { ModeToggle } from './ModeToggle'; import { UnifiedWalletButton } from './UnifiedWalletButton'; export function Header() { + const location = useLocation(); + + const navLinkClass = (path: string) => + `px-4 py-2 rounded-md text-sm font-medium transition-colors h-10 inline-flex items-center ${ + location.pathname === path + ? 'text-foreground' + : 'text-muted-foreground hover:text-foreground' + }`; + return ( -
- -

- Zoro logo -

+
+ + Zoro + + zoro swap + -
- -
- Swap -
+
-
+ +
-
- -
+
-
+
); } diff --git a/src/components/LiquidityPoolRow.tsx b/src/components/LiquidityPoolRow.tsx index f976a9f..e9979ef 100644 --- a/src/components/LiquidityPoolRow.tsx +++ b/src/components/LiquidityPoolRow.tsx @@ -2,80 +2,98 @@ import type { PoolBalance } from '@/hooks/usePoolsBalances'; import type { PoolInfo } from '@/hooks/usePoolsInfo'; import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; import type { TokenConfig } from '@/providers/ZoroProvider'; -import { formalBigIntFormat, prettyBigintFormat } from '@/utils/format'; +import { prettyBigintFormat } from '@/utils/format'; import AssetIcon from './AssetIcon'; -import Price from './Price'; import { Button } from './ui/button'; +const feeTierForSymbol = (symbol: string) => + /USDC|USDT|DAI|BUSD/i.test(symbol) ? '0.01%' : '0.30%'; + const LiquidityPoolRow = ({ pool, - tokenConfig, poolBalances, managePool, className, lpBalance, + variant = 'manage', }: { pool: PoolInfo; - poolBalances: PoolBalance; tokenConfig?: TokenConfig; + poolBalances: PoolBalance; lpBalance: bigint; managePool: (pool: PoolInfo) => void; className?: string; + variant?: 'manage' | 'addLiquidity'; }) => { const { connected: isConnected } = useUnifiedWallet(); const decimals = pool.decimals; + const feeTier = feeTierForSymbol(pool.symbol); + const tvlFormatted = prettyBigintFormat({ + value: poolBalances.totalLiabilities, + expo: decimals, + }); - const saturation = - ((poolBalances.reserve * BigInt(1e8)) / poolBalances.totalLiabilities) - / BigInt(1e6); + if (variant === 'addLiquidity') { + return ( + + +
+
+ + + + + + +
+
+ {pool.name} + {feeTier} +
+
+ + ${tvlFormatted} + — + — + — + + + + + ); + } return ( - - + +

{pool.name}

-

- ${tokenConfig && } -

- n / a - - {prettyBigintFormat({ value: poolBalances.totalLiabilities, expo: decimals })} - {' '} - - / Inf - - - - {formalBigIntFormat({ - val: saturation, - expo: 0, - round: 2, - })} % - - { - /* - {pool.apr24h === 0 ? '<0.01' : pool.apr24h} % /{' '} - {pool.apr7d === 0 ? '<0.01' : pool.apr7d} % - */ - } - - {prettyBigintFormat({ value: lpBalance, expo: decimals })}{' '} - z{pool.symbol} + ${tvlFormatted} + n / a + — + — + + {prettyBigintFormat({ value: lpBalance, expo: decimals })}{' '} + z{pool.symbol} - + diff --git a/src/components/LiquidityPoolsTable.tsx b/src/components/LiquidityPoolsTable.tsx index 688053b..edb955b 100644 --- a/src/components/LiquidityPoolsTable.tsx +++ b/src/components/LiquidityPoolsTable.tsx @@ -1,113 +1,124 @@ -import { useLPBalances } from '@/hooks/useLPBalances'; -import { usePoolsBalances } from '@/hooks/usePoolsBalances'; -import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; -import { useOrderUpdates } from '@/hooks/useWebSocket'; -import { ModalContext } from '@/providers/ModalContext'; -import { ZoroContext } from '@/providers/ZoroContext'; -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import type { PoolBalance } from '@/hooks/usePoolsBalances'; +import { type PoolInfo } from '@/hooks/usePoolsInfo'; +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { useMemo, useState } from 'react'; import LiquidityPoolRow from './LiquidityPoolRow'; -import { type LpDetails, OrderStatus, type TxResult } from './OrderStatus'; -import PoolModal from './PoolModal'; import { poweredByMiden } from './PoweredByMiden'; import { Card } from './ui/card'; +import { Input } from './ui/input'; +import { Button } from './ui/button'; +import { Search, Flame, Clock, Star } from 'lucide-react'; -const LiquidityPoolsTable = () => { - const { data: poolsInfo, refetch: refetchPoolsInfo } = usePoolsInfo(); - const { data: poolBalances, refetch: refetchPoolBalances } = usePoolsBalances(); - const modalContext = useContext(ModalContext); - const lastShownNoteId = useRef(undefined); - const [txResult, setTxResult] = useState(); - const [lpDetails, setLpDetails] = useState(undefined); - const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); - const { orderStatus, registerCallback } = useOrderUpdates(); - const { tokens } = useContext(ZoroContext); +export interface LiquidityPoolsTableProps { + poolsInfo?: { poolAccountId?: string; liquidityPools?: PoolInfo[] } | null; + poolBalances?: PoolBalance[] | null; + lpBalances: Record; + tokenConfigs?: (TokenConfig | undefined)[]; + openPoolModal: (pool: PoolInfo) => void; +} - const tokenConfigs = useMemo( - () => poolsInfo?.liquidityPools?.map(p => tokens[p.faucetIdBech32]), - [tokens, poolsInfo?.liquidityPools], - ); - - const openOrderStatusModal = useCallback((noteId: string) => { - lastShownNoteId.current = noteId; - setIsSuccessModalOpen(true); - }, []); +const LiquidityPoolsTable = ({ + poolsInfo, + poolBalances, + lpBalances, + tokenConfigs, + openPoolModal, +}: LiquidityPoolsTableProps) => { + const [search, setSearch] = useState(''); + const [poolFilter, setPoolFilter] = useState<'all' | 'hot' | 'new' | 'stables'>('all'); - const { balances: lpBalances, refetch: refetchLpBalances } = useLPBalances({ - tokens: tokenConfigs, - }); - - useEffect(() => { - if (txResult?.noteId) { - registerCallback(txResult.noteId, status => { - if (status === 'executed') { - refetchLpBalances(); - refetchPoolBalances(); - } - }); - } - }, [ - orderStatus, - txResult?.noteId, - refetchPoolBalances, - refetchLpBalances, - registerCallback, - ]); - - const openPoolManagementModal = useCallback( - (pool: PoolInfo) => { - modalContext.openModal( - , + const filteredPools = useMemo(() => { + const pools = poolsInfo?.liquidityPools ?? []; + let list = pools; + if (search.trim()) { + const q = search.trim().toLowerCase(); + list = list.filter( + p => + p.name.toLowerCase().includes(q) || + p.symbol.toLowerCase().includes(q), ); - }, - [modalContext, refetchPoolsInfo, openOrderStatusModal, lpBalances], - ); + } + if (poolFilter === 'stables') { + list = list.filter(p => /USDC|USDT|DAI|BUSD/i.test(p.symbol)); + } + return list; + }, [poolsInfo?.liquidityPools, search, poolFilter]); return ( -
- -

Liquidity Pools

-
- +
+
+
+ + setSearch(e.target.value)} + className='pl-9 rounded-lg bg-muted/50 border-muted-foreground/20' + /> +
+
+ + + +
+
+ + +
+
- - - - - - - + + + + + + + - {poolsInfo?.liquidityPools?.map(p => { - const balances = poolBalances?.find(b => - b.faucetIdBech32 == p.faucetIdBech32 - ); - const tokenConfig = tokenConfigs?.find(c => - c.faucetIdBech32 === p.faucetIdBech32 - ); + {filteredPools.map(pool => { + const balances = poolBalances?.find(b => b.faucetIdBech32 === pool.faucetIdBech32); + const tokenConfig = tokenConfigs?.find(c => c?.faucetIdBech32 === pool.faucetIdBech32); return balances ? ( ) : ( - - + + ); })} @@ -115,20 +126,10 @@ const LiquidityPoolsTable = () => {
Apr(24h / 7d)TVL / CapSaturationMy position
PoolTVL ↑APR ↑1D VOL ↑7D VOL ↑
-
+
{poweredByMiden}
- {isSuccessModalOpen && ( - setIsSuccessModalOpen(false)} - swapResult={txResult} - lpDetails={lpDetails} - orderStatus={txResult?.noteId - ? orderStatus[txResult.noteId]?.status - : undefined} - /> - )}
); }; diff --git a/src/components/PositionCard.tsx b/src/components/PositionCard.tsx new file mode 100644 index 0000000..efc0af6 --- /dev/null +++ b/src/components/PositionCard.tsx @@ -0,0 +1,87 @@ +import type { PoolBalance } from '@/hooks/usePoolsBalances'; +import type { PoolInfo } from '@/hooks/usePoolsInfo'; +import { prettyBigintFormat } from '@/utils/format'; +import AssetIcon from './AssetIcon'; +import { Button } from './ui/button'; +import { Card, CardContent } from './ui/card'; + +interface PositionCardProps { + pool: PoolInfo; + poolBalance: PoolBalance; + lpBalance: bigint; + feeTier?: string; + onDeposit: () => void; + onWithdraw: () => void; + disabled?: boolean; +} + +export function PositionCard({ + pool, + lpBalance, + feeTier = '0.30%', + onDeposit, + onWithdraw, + disabled = false, +}: PositionCardProps) { + const decimals = pool.decimals; + const liquidityFormatted = prettyBigintFormat({ + value: lpBalance, + expo: decimals, + }); + + return ( + + +
+
+ + + + + + +
+ {pool.name} + {feeTier} +
+
+
+ Liquidity + ${liquidityFormatted} +
+
+ Fees earned + +
+
+ {pool.symbol} + {liquidityFormatted} +
+
+ USDC + +
+
+
+ + +
+
+
+ ); +} diff --git a/src/pages/LiquidityPools.tsx b/src/pages/LiquidityPools.tsx index 7181bbb..b1a3e30 100644 --- a/src/pages/LiquidityPools.tsx +++ b/src/pages/LiquidityPools.tsx @@ -1,8 +1,95 @@ import { Footer } from '@/components/Footer'; import { Header } from '@/components/Header'; import LiquidityPoolsTable from '@/components/LiquidityPoolsTable'; +import { PositionCard } from '@/components/PositionCard'; +import { AllDropdown } from '@/components/AllDropdown'; +import { useLPBalances } from '@/hooks/useLPBalances'; +import { usePoolsBalances } from '@/hooks/usePoolsBalances'; +import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; +import { useOrderUpdates } from '@/hooks/useWebSocket'; +import { ModalContext } from '@/providers/ModalContext'; +import { ZoroContext } from '@/providers/ZoroContext'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { OrderStatus, type LpDetails, type TxResult } from '@/components/OrderStatus'; +import PoolModal from '@/components/PoolModal'; +import { Button } from '@/components/ui/button'; function LiquidityPools() { + const { data: poolsInfo, refetch: refetchPoolsInfo } = usePoolsInfo(); + const { data: poolBalances, refetch: refetchPoolBalances } = usePoolsBalances(); + const modalContext = useContext(ModalContext); + const { tokens } = useContext(ZoroContext); + const { orderStatus, registerCallback } = useOrderUpdates(); + const lastShownNoteId = useRef(undefined); + const [txResult, setTxResult] = useState(); + const [lpDetails, setLpDetails] = useState(undefined); + const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); + + const tokenConfigs = useMemo( + () => poolsInfo?.liquidityPools?.map(p => tokens[p.faucetIdBech32]), + [tokens, poolsInfo?.liquidityPools], + ); + + const { balances: lpBalances, refetch: refetchLpBalances } = useLPBalances({ + tokens: tokenConfigs, + }); + + const openOrderStatusModal = useCallback((noteId: string) => { + lastShownNoteId.current = noteId; + setIsSuccessModalOpen(true); + }, []); + + useEffect(() => { + if (txResult?.noteId) { + registerCallback(txResult.noteId, status => { + if (status === 'executed') { + refetchLpBalances(); + refetchPoolBalances(); + } + }); + } + }, [ + txResult?.noteId, + refetchPoolBalances, + refetchLpBalances, + registerCallback, + ]); + + const openPoolModal = useCallback( + (pool: PoolInfo) => { + modalContext.openModal( + , + ); + }, + [modalContext, refetchPoolsInfo, openOrderStatusModal, lpBalances], + ); + + const userPositions = useMemo(() => { + if (!poolsInfo?.liquidityPools || !poolBalances) return []; + return poolsInfo.liquidityPools + .map(pool => { + const balance = poolBalances.find(b => b.faucetIdBech32 === pool.faucetIdBech32); + const lp = lpBalances[pool.faucetIdBech32] ?? BigInt(0); + if (!balance || lp <= BigInt(0)) return null; + return { pool, poolBalance: balance, lpBalance: lp }; + }) + .filter((x): x is NonNullable => x !== null); + }, [poolsInfo?.liquidityPools, poolBalances, lpBalances]); + return (
Pools - ZoroSwap | DeFi on Miden @@ -10,21 +97,68 @@ function LiquidityPools() { name='description' content='Deposit to ZoroSwap pools to earn attractive yield' /> - - - -
-
- +
+ {/* Your positions */} +
+
+

+ Your positions +

+
+ + + +
+
+
+ {userPositions.length > 0 + ? userPositions.map(({ pool, poolBalance, lpBalance }) => ( + openPoolModal(pool)} + onWithdraw={() => openPoolModal(pool)} + /> + )) + : ( +
+ No positions yet. Add liquidity in Existing Pools below. +
+ )} +
+
+ + {/* Existing Pools */} +
+

+ Existing Pools +

+ +
+ {isSuccessModalOpen && ( + setIsSuccessModalOpen(false)} + swapResult={txResult} + lpDetails={lpDetails} + orderStatus={ + txResult?.noteId ? orderStatus[txResult.noteId]?.status : undefined + } + /> + )}
); } From 9ce7761a0b910365807d4a170caa27f4c322360b Mon Sep 17 00:00:00 2001 From: mico Date: Mon, 2 Mar 2026 19:12:30 +0100 Subject: [PATCH 02/49] Detail page skeleton --- src/App.tsx | 3 +- src/components/Header.tsx | 4 +- src/components/LiquidityPoolRow.tsx | 35 +- src/components/LiquidityPoolsTable.tsx | 3 + src/components/OrderStatus.tsx | 138 ++++++- src/components/PoolDetailView.tsx | 128 +++++++ src/components/PoolModal.tsx | 480 ++++++++++++++++--------- src/components/PositionCard.tsx | 9 +- src/hooks/usePoolsInfo.tsx | 17 +- src/pages/LiquidityPools.tsx | 21 +- src/pages/PoolDetail.tsx | 391 ++++++++++++++++++++ 11 files changed, 1036 insertions(+), 193 deletions(-) create mode 100644 src/components/PoolDetailView.tsx create mode 100644 src/pages/PoolDetail.tsx diff --git a/src/App.tsx b/src/App.tsx index 468d139..796913c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import '@demox-labs/miden-wallet-adapter-reactui/styles.css'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Bounce, ToastContainer } from 'react-toastify'; import LiquidityPools from './pages/LiquidityPools'; +import PoolDetail from './pages/PoolDetail'; import ModalProvider from './providers/ModalProvider'; import { ZoroProvider } from './providers/ZoroProvider'; import { ParaProviderWrapper } from './providers/ParaProviderWrapper'; @@ -28,7 +29,7 @@ function AppRouter() { } /> } /> } /> - } /> + } /> } /> diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 162a50e..9b74080 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -32,8 +32,8 @@ export function Header() { Explore - - Pools + + Faucet
diff --git a/src/components/LiquidityPoolRow.tsx b/src/components/LiquidityPoolRow.tsx index e9979ef..8326ddc 100644 --- a/src/components/LiquidityPoolRow.tsx +++ b/src/components/LiquidityPoolRow.tsx @@ -16,6 +16,7 @@ const LiquidityPoolRow = ({ className, lpBalance, variant = 'manage', + onRowClick, }: { pool: PoolInfo; tokenConfig?: TokenConfig; @@ -24,6 +25,7 @@ const LiquidityPoolRow = ({ managePool: (pool: PoolInfo) => void; className?: string; variant?: 'manage' | 'addLiquidity'; + onRowClick?: (pool: PoolInfo) => void; }) => { const { connected: isConnected } = useUnifiedWallet(); const decimals = pool.decimals; @@ -33,9 +35,26 @@ const LiquidityPoolRow = ({ expo: decimals, }); + const isRowClickable = variant === 'addLiquidity' && onRowClick; + if (variant === 'addLiquidity') { return ( - + onRowClick?.(pool) : undefined} + onKeyDown={ + isRowClickable + ? (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onRowClick?.(pool); + } + } + : undefined + } + >
@@ -46,9 +65,14 @@ const LiquidityPoolRow = ({
-
+
{pool.name} - {feeTier} + {pool.poolType && ( + + {pool.poolType} + + )} + {feeTier}
@@ -56,7 +80,10 @@ const LiquidityPoolRow = ({ — — — - + e.stopPropagation()} + > +
+
+
+ +
+

+ {successTitle} +

+

+ {successMessage} +

+
+
+
+
+ + + + + + +
+ + {lpDetails.token?.symbol} / USDC + +
+ {lpDetails.actionType === 'Deposit' ? ( + <> +

+ Liquidity Added +

+
+ + {lpDetails.token?.symbol} + +{amountFormatted} +
+
+ + USDC + +— +
+ + ) : ( + <> +
+ {lpDetails.token?.symbol} + {amountFormatted} +
+
+ USDC + +
+
+ Fees Claimed + +$0.00 +
+ + )} +
+
+ +
+ + + + +
+
+ + + ) : ( + <>
{title}
- {/* Order Status */}
+ + )}
diff --git a/src/components/PoolDetailView.tsx b/src/components/PoolDetailView.tsx new file mode 100644 index 0000000..708c53f --- /dev/null +++ b/src/components/PoolDetailView.tsx @@ -0,0 +1,128 @@ +import type { PoolBalance } from '@/hooks/usePoolsBalances'; +import type { PoolInfo } from '@/hooks/usePoolsInfo'; +import { prettyBigintFormat } from '@/utils/format'; +import { X } from 'lucide-react'; +import AssetIcon from './AssetIcon'; +import { Button } from './ui/button'; + +interface PoolDetailViewProps { + pool: PoolInfo; + poolBalance: PoolBalance; + lpBalance: bigint; + onAddLiquidity: () => void; + onWithdraw: () => void; + onClose: () => void; +} + +export function PoolDetailView({ + pool, + poolBalance, + lpBalance, + onAddLiquidity, + onWithdraw, + onClose, +}: PoolDetailViewProps) { + const decimals = pool.decimals; + const tvlFormatted = prettyBigintFormat({ + value: poolBalance.totalLiabilities, + expo: decimals, + }); + const hasPosition = lpBalance > BigInt(0); + + return ( +
+
+
+
+ + + + + + +
+
+
+

{pool.name}

+ {pool.poolType && ( + + {pool.poolType} + + )} +
+

+ {pool.symbol} / USDC +

+
+
+ +
+ +
+
+

+ TVL +

+

${tvlFormatted}

+
+
+

+ APR +

+

+
+
+

+ 1D VOL +

+

+
+
+

+ 7D VOL +

+

+
+
+ + {hasPosition && ( +
+

+ Your position +

+

+ {prettyBigintFormat({ value: lpBalance, expo: decimals })}{' '} + z{pool.symbol} +

+
+ )} + +
+ + {hasPosition && ( + + )} +
+
+ ); +} diff --git a/src/components/PoolModal.tsx b/src/components/PoolModal.tsx index b571632..95a32c5 100644 --- a/src/components/PoolModal.tsx +++ b/src/components/PoolModal.tsx @@ -3,7 +3,7 @@ import { useWithdraw } from '@/hooks/useWithdraw'; import { ZoroContext } from '@/providers/ZoroContext'; import type { TokenConfig } from '@/providers/ZoroProvider'; import { NoteType } from '@miden-sdk/miden-sdk'; -import { Loader, X } from 'lucide-react'; +import { ChevronDown, Info, Loader, AlertTriangle, X } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { parseUnits } from 'viem'; import { useBalance } from '../hooks/useBalance'; @@ -11,7 +11,7 @@ import { type PoolInfo } from '../hooks/usePoolsInfo'; import { ModalContext } from '../providers/ModalContext'; import { formatTokenAmount } from '../utils/format'; import type { LpDetails, TxResult } from './OrderStatus'; -import Slippage from './Slippage'; +import AssetIcon from './AssetIcon'; import { Button } from './ui/button'; import { Input } from './ui/input'; @@ -22,56 +22,58 @@ interface PoolModalProps { setLpDetails: (lpDetails: LpDetails) => void; onSuccess: (noteId: string) => void; lpBalance: bigint; + initialMode?: LpActionType; } -const validateValue = (val: bigint, max: bigint) => { - return val > max - ? 'Amount too large' - : val <= 0 - ? 'Invalid value' - : undefined; -}; +const validateValue = (val: bigint, max: bigint) => + val > max ? 'Amount too large' : val <= BigInt(0) ? 'Invalid value' : undefined; export type LpActionType = 'Deposit' | 'Withdraw'; -const PoolModal = ( - { pool, refetchPoolInfo, setTxResult, setLpDetails, onSuccess, lpBalance }: - PoolModalProps, -) => { +const PERCENTAGES = [25, 50, 75, 100] as const; + +export default function PoolModal({ + pool, + refetchPoolInfo, + setTxResult, + setLpDetails, + onSuccess, + lpBalance, + initialMode = 'Deposit', +}: PoolModalProps) { const modalContext = useContext(ModalContext); const { tokens } = useContext(ZoroContext); - const [mode, setMode] = useState('Deposit'); + const [mode, setMode] = useState(initialMode); const [rawValue, setRawValue] = useState(BigInt(0)); const [inputError, setInputError] = useState(undefined); const [inputValue, setInputValue] = useState(''); - const [slippage, setSlippage] = useState(0.5); + const [withdrawPct, setWithdrawPct] = useState(100); + const token = useMemo( - () => Object.values(tokens).find(t => t.faucetIdBech32 === pool.faucetIdBech32), + () => + Object.values(tokens).find((t) => t.faucetIdBech32 === pool.faucetIdBech32), [tokens, pool.faucetIdBech32], ); + const quoteToken = useMemo( + () => Object.values(tokens).find((t) => t.symbol === 'USDC'), + [tokens], + ); const { balance: balanceToken, refetch: refetchBalanceToken } = useBalance({ token, }); - const balance = useMemo( - () => - mode === 'Withdraw' - ? lpBalance ?? BigInt(0) - : balanceToken ?? BigInt(0), - [balanceToken, lpBalance, mode], - ); + const { balance: balanceQuote } = useBalance({ token: quoteToken ?? undefined }); + const balance = + mode === 'Withdraw' ? lpBalance ?? BigInt(0) : balanceToken ?? BigInt(0); const decimals = pool.decimals; const clearForm = useCallback(() => { setInputValue(''); setRawValue(BigInt(0)); + setWithdrawPct(100); refetchBalanceToken().catch(console.error); refetchPoolInfo?.(); - }, [ - refetchBalanceToken, - refetchPoolInfo, - ]); + }, [refetchBalanceToken, refetchPoolInfo]); - // DEPOSITING const { deposit, isLoading: isDepositLoading, @@ -79,8 +81,6 @@ const PoolModal = ( txId: depositTxId, noteId: depositNoteId, } = useDeposit(); - - // WITHDRAWING const { withdraw, isLoading: isWithdrawLoading, @@ -96,9 +96,10 @@ const PoolModal = ( amount: rawValue, actionType: mode, }); - const txResult = mode === 'Deposit' - ? { txId: depositTxId, noteId: depositNoteId } - : { txId: withdrawTxId, noteId: withdrawNoteId }; + const txResult = + mode === 'Deposit' + ? { txId: depositTxId, noteId: depositNoteId } + : { txId: withdrawTxId, noteId: withdrawNoteId }; setTxResult(txResult); clearForm(); onSuccess(txResult.noteId as string); @@ -123,10 +124,6 @@ const PoolModal = ( if (token == null) return; await deposit({ amount: rawValue, - - // TODO: This needs to be the correct LP amount, not token amount - // BigInt(rawValue * BigInt(1e8 - 1e6 * slippage) / BigInt(1e8)) - // find a way to simulate it properly minAmountOut: BigInt(1), token, noteType: NoteType.Public, @@ -135,12 +132,8 @@ const PoolModal = ( const writeWithdraw = useCallback(async () => { if (token == null) return; - await withdraw({ amount: rawValue, - // TODO: This needs to be the correct LP amount, not token amount - // BigInt(rawValue * BigInt(1e8 - 1e6 * slippage) / BigInt(1e8)) - // find a way to simulate it properly minAmountOut: BigInt(1), token, noteType: NoteType.Public, @@ -155,156 +148,301 @@ const PoolModal = ( setInputValue( (formatTokenAmount({ value: newValue, expo: decimals }) ?? '').toString(), ); + if (mode === 'Withdraw') setWithdrawPct(percentage); }, - [decimals, balance], + [decimals, balance, mode], ); - const onInputChange = useCallback((val: string) => { - setInputValue(val); - if (val === '') { - setInputError(undefined); - setRawValue(BigInt(0)); - return; - } - const parsed = parseUnits(val, decimals); - const validationError = validateValue(parsed, balance); - if (validationError) { - setInputError(validationError); - } else { - setInputError(undefined); - setRawValue(parseUnits(val, decimals)); + + const onInputChange = useCallback( + (val: string) => { + setInputValue(val); + if (val === '') { + setInputError(undefined); + setRawValue(BigInt(0)); + if (mode === 'Withdraw') setWithdrawPct(0); + return; + } + const parsed = parseUnits(val, decimals); + const validationError = validateValue(parsed, balance); + if (validationError) setInputError(validationError); + else { + setInputError(undefined); + setRawValue(parsed); + if (mode === 'Withdraw' && balance > BigInt(0)) { + const pct = Number((parsed * BigInt(100)) / balance); + setWithdrawPct(Math.min(100, Math.max(0, pct))); + } + } + }, + [decimals, balance, mode], + ); + + useEffect(() => { + if (mode === 'Withdraw' && balance > BigInt(0)) { + const newValue = (BigInt(withdrawPct) * balance) / BigInt(100); + setRawValue(newValue); + setInputValue( + (formatTokenAmount({ value: newValue, expo: decimals }) ?? '').toString(), + ); } - }, [decimals, balance]); + }, [withdrawPct, mode, balance, decimals]); + + const handleClose = useCallback(() => modalContext.closeModal(), [modalContext]); - const handleClose = useCallback(() => { - modalContext.closeModal(); - }, [modalContext]); + const poolLabel = pool.name || `${pool.symbol} / USDC`; + const withdrawReceiveAmount = rawValue; + const withdrawReceiveFormatted = formatTokenAmount({ + value: withdrawReceiveAmount, + expo: decimals, + }); + const totalValueUsd = '—'; return ( -
-
-
+
+
+
+ + + + + + +
+ + {mode === 'Withdraw' ? `Withdraw from ${poolLabel}` : poolLabel} + +
+ +
+ +
+
-
+
-
- -
{' '} -
-

- {mode === 'Deposit' ? 'Deposit amount' : 'Withdrawal amount'} -

-
- { - onInputChange(e.target.value); - }} - /> -
- {token?.symbol} -
-
- {inputError ?

{inputError}

: null} -
- {[25, 50, 75, 100].map(n => ( - - ))} -
+
-
-
-

Max slippage

-
- - {slippage} % + + {mode === 'Deposit' && ( + <> +
+

+ Deposit amounts +

+
+

Amount

+
+ onInputChange(e.target.value)} + /> +
+ + Balance:{' '} + {formatTokenAmount({ + value: balanceToken, + expo: pool.decimals, + })}{' '} + {pool.symbol} + + + + +
+
+
+ {PERCENTAGES.map((n) => ( + + ))} +
+
+
+ +
+
+

Amount

+
+ 0.00 +
+ + Balance:{' '} + {formatTokenAmount({ + value: balanceQuote, + expo: quoteToken?.decimals ?? 6, + })}{' '} + USDC + + +
+
+
-
-

- Balance - - {formatTokenAmount({ - value: balanceToken, - expo: pool.decimals, - })} {pool.symbol} - -

-

- My position - - {formatTokenAmount({ - value: lpBalance, - expo: pool.decimals, - })} z{pool.symbol} - + {inputError && ( +

{inputError}

+ )} +
+ +
+ Pool Share + ~0.01% +
+
+ Est. APR + 24.5% +
+
+ + + )} + + {mode === 'Withdraw' && ( + <> +
+
+

+ Withdraw amount +

+ {withdrawPct}% +
+
+
+
+
+ {PERCENTAGES.map((n) => ( + + ))} +
+
+
+

+ You'll receive +

+
+
+ + {pool.symbol} +
+ {withdrawReceiveFormatted ?? '0'} +
+
+
+ + USDC +
+ +
+
+ Total Value + ${totalValueUsd} +
+
+
+ +
+

+ Impermanent Loss Notice +

+

+ Withdrawing now realizes any impermanent loss. Your position may + have experienced IL since deposit. If you deposited at a + different price ratio, you may receive fewer tokens than + expected. +

+
+
+
+
+ Unclaimed Fees + +$0.00 +
+
+ Network Fee + +
+
+ + + )} + + {(depositError || withdrawError) && ( +

+ {depositError ?? withdrawError}

-
-
- {mode === 'Deposit' - ? ( - - ) - : null} - {mode === 'Withdraw' - ? ( - - ) - : null} -
- {depositError ?

{depositError}

: null} - {withdrawError ?

{withdrawError}

: null} + )}
); -}; - -export default PoolModal; +} diff --git a/src/components/PositionCard.tsx b/src/components/PositionCard.tsx index efc0af6..ef83f8a 100644 --- a/src/components/PositionCard.tsx +++ b/src/components/PositionCard.tsx @@ -42,12 +42,17 @@ export function PositionCard({
{pool.name} + {pool.poolType && ( + + {pool.poolType} + + )} {feeTier}
- Liquidity - ${liquidityFormatted} + Your deposit + {liquidityFormatted}
Fees earned diff --git a/src/hooks/usePoolsInfo.tsx b/src/hooks/usePoolsInfo.tsx index 7f2a6cf..da1f135 100644 --- a/src/hooks/usePoolsInfo.tsx +++ b/src/hooks/usePoolsInfo.tsx @@ -4,6 +4,8 @@ import type { AccountId } from '@miden-sdk/miden-sdk'; import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +export type PoolType = 'hfAMM' | 'xyk'; + export interface RawPoolInfo { decimals: number; faucet_id: string; @@ -18,6 +20,7 @@ export interface PoolInfo { name: string; oracleId: string; symbol: string; + poolType: PoolType; } export const usePoolsInfo = () => { @@ -32,12 +35,14 @@ export const usePoolsInfo = () => { data: { poolAccountId: data?.pool_account_id, liquidityPools: data?.liquidity_pools.map( - p => ({ - ...p, - oracleId: p.oracle_id, - faucetId: bech32ToAccountId(p.faucet_id), - faucetIdBech32: p.faucet_id, - } as PoolInfo), + p => + ({ + ...p, + oracleId: p.oracle_id, + faucetId: bech32ToAccountId(p.faucet_id), + faucetIdBech32: p.faucet_id, + poolType: 'hfAMM' as const, + }) as PoolInfo, ), }, refetch: refetch, diff --git a/src/pages/LiquidityPools.tsx b/src/pages/LiquidityPools.tsx index b1a3e30..2a73e77 100644 --- a/src/pages/LiquidityPools.tsx +++ b/src/pages/LiquidityPools.tsx @@ -17,11 +17,14 @@ import { useRef, useState, } from 'react'; +import { useNavigate } from 'react-router-dom'; import { OrderStatus, type LpDetails, type TxResult } from '@/components/OrderStatus'; import PoolModal from '@/components/PoolModal'; +import type { LpActionType } from '@/components/PoolModal'; import { Button } from '@/components/ui/button'; function LiquidityPools() { + const navigate = useNavigate(); const { data: poolsInfo, refetch: refetchPoolsInfo } = usePoolsInfo(); const { data: poolBalances, refetch: refetchPoolBalances } = usePoolsBalances(); const modalContext = useContext(ModalContext); @@ -63,7 +66,7 @@ function LiquidityPools() { ]); const openPoolModal = useCallback( - (pool: PoolInfo) => { + (pool: PoolInfo, initialMode?: LpActionType) => { modalContext.openModal( , ); }, [modalContext, refetchPoolsInfo, openOrderStatusModal, lpBalances], ); + const onPoolRowClick = useCallback( + (pool: PoolInfo) => { + navigate(`/explore/pool/${encodeURIComponent(pool.faucetIdBech32)}`); + }, + [navigate], + ); + const userPositions = useMemo(() => { if (!poolsInfo?.liquidityPools || !poolBalances) return []; return poolsInfo.liquidityPools - .map(pool => { - const balance = poolBalances.find(b => b.faucetIdBech32 === pool.faucetIdBech32); + .filter((pool) => pool.poolType === 'hfAMM') + .map((pool) => { + const balance = poolBalances.find((b) => b.faucetIdBech32 === pool.faucetIdBech32); const lp = lpBalances[pool.faucetIdBech32] ?? BigInt(0); if (!balance || lp <= BigInt(0)) return null; return { pool, poolBalance: balance, lpBalance: lp }; @@ -99,7 +111,6 @@ function LiquidityPools() { />
- {/* Your positions */}

@@ -133,7 +144,6 @@ function LiquidityPools() {

- {/* Existing Pools */}

Existing Pools @@ -144,6 +154,7 @@ function LiquidityPools() { lpBalances={lpBalances} tokenConfigs={tokenConfigs} openPoolModal={openPoolModal} + onPoolRowClick={onPoolRowClick} />

diff --git a/src/pages/PoolDetail.tsx b/src/pages/PoolDetail.tsx new file mode 100644 index 0000000..7e72149 --- /dev/null +++ b/src/pages/PoolDetail.tsx @@ -0,0 +1,391 @@ +import { Footer } from '@/components/Footer'; +import { Header } from '@/components/Header'; +import { OrderStatus, type LpDetails, type TxResult } from '@/components/OrderStatus'; +import PoolModal from '@/components/PoolModal'; +import type { LpActionType } from '@/components/PoolModal'; +import { useLPBalances } from '@/hooks/useLPBalances'; +import { usePoolsBalances } from '@/hooks/usePoolsBalances'; +import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; +import { useOrderUpdates } from '@/hooks/useWebSocket'; +import { ModalContext } from '@/providers/ModalContext'; +import { ZoroContext } from '@/providers/ZoroContext'; +import { prettyBigintFormat, truncateId } from '@/utils/format'; +import { AlertTriangle, ExternalLink, ArrowLeft } from 'lucide-react'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Link, useParams } from 'react-router-dom'; +import AssetIcon from '@/components/AssetIcon'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +const feeTierForSymbol = (symbol: string) => + /USDC|USDT|DAI|BUSD/i.test(symbol) ? '0.01%' : '0.30%'; + +const PLACEHOLDER_TXS = [ + { type: 'Swap' as const, in: '0.5 ETH', out: '1,090 USDC', account: '0x1a2b...3c4d', time: '2 min ago' }, + { type: 'Add' as const, in: '1.0 ETH', out: '2,180 USDC', account: '0x5e6f...7a8b', time: '8 min ago' }, + { type: 'Remove' as const, in: '0.25 ETH', out: '545 USDC', account: '0x9c0d...1e2f', time: '14 min ago' }, + { type: 'Swap' as const, in: '2,500 USDC', out: '1.14 ETH', account: '0x3a4b...5c6d', time: '22 min ago' }, + { type: 'Swap' as const, in: '0.1 ETH', out: '218 USDC', account: '0x7e8f...9a0b', time: '31 min ago' }, +]; + +const TYPE_COLORS: Record = { + Swap: 'text-primary', + Add: 'text-green-600', + Remove: 'text-amber-600', +}; + +export default function PoolDetail() { + const { poolId } = useParams<{ poolId: string }>(); + const { data: poolsInfo, refetch: refetchPoolsInfo } = usePoolsInfo(); + const { data: poolBalances } = usePoolsBalances(); + const modalContext = useContext(ModalContext); + const { tokens } = useContext(ZoroContext); + const { orderStatus, registerCallback } = useOrderUpdates(); + const lastShownNoteId = useRef(undefined); + const [txResult, setTxResult] = useState(); + const [lpDetails, setLpDetails] = useState(undefined); + const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); + const [chartRange, setChartRange] = useState<'1D' | '1W' | '1M' | 'ALL'>('1W'); + + const tokenConfigs = useMemo( + () => poolsInfo?.liquidityPools?.map((p) => tokens[p.faucetIdBech32]), + [tokens, poolsInfo?.liquidityPools], + ); + const { balances: lpBalances, refetch: refetchLpBalances } = useLPBalances({ + tokens: tokenConfigs, + }); + + const pool = useMemo(() => { + if (!poolId || !poolsInfo?.liquidityPools) return null; + return poolsInfo.liquidityPools.find( + (p) => p.faucetIdBech32 === decodeURIComponent(poolId), + ) ?? null; + }, [poolId, poolsInfo?.liquidityPools]); + + const poolBalance = useMemo(() => { + if (!pool || !poolBalances) return null; + return poolBalances.find((b) => b.faucetIdBech32 === pool.faucetIdBech32) ?? null; + }, [pool, poolBalances]); + + const lpBalance = pool ? lpBalances[pool.faucetIdBech32] ?? BigInt(0) : BigInt(0); + const hasPosition = lpBalance > BigInt(0); + + const openOrderStatusModal = useCallback((noteId: string) => { + lastShownNoteId.current = noteId; + setIsSuccessModalOpen(true); + }, []); + + useEffect(() => { + if (txResult?.noteId) { + registerCallback(txResult.noteId, (status) => { + if (status === 'executed') { + refetchLpBalances(); + } + }); + } + }, [txResult?.noteId, refetchLpBalances, registerCallback]); + + const openPoolModal = useCallback( + (p: PoolInfo, initialMode?: LpActionType) => { + modalContext.openModal( + , + ); + }, + [modalContext, refetchPoolsInfo, openOrderStatusModal, lpBalances], + ); + + if (!pool || !poolBalance) { + return ( +
+
+
+

Pool not found.

+ + ← Back to pools + +
+
+
+ ); + } + + const decimals = pool.decimals; + const feeTier = feeTierForSymbol(pool.symbol); + const tvlFormatted = prettyBigintFormat({ + value: poolBalance.totalLiabilities, + expo: decimals, + }); + const pairLabel = `${pool.symbol} / USDC`; + + return ( +
+ {pairLabel} - ZoroSwap +
+
+ + + Back to pools + + +
+
+
+ + + + + + +
+
+

{pairLabel}

+ +
+
+
+ + +
+
+ +
+ + +

+ Total Value Locked +

+

${tvlFormatted}

+
+
+ + +

+ APR (est.) +

+

24.5%

+
+
+ + +

+ 24H Volume +

+

$12,500

+
+
+ + +

+ 24H Fees +

+

$37.50

+
+
+
+ +
+
+ + + Pool Composition + + +
+
+ + {pool.symbol} +
+
+

21.56

+

$2,180.00

+
+
+
+
+ + USDC +
+
+

45,020.00

+

$1.00

+
+
+
+
+
+
+

+ ETH 48% · USDC 52% +

+ + + + + + Pool Info + + +
+ Fee Tier + {feeTier} +
+
+ 7D Volume + $110,800 +
+
+ 24h Transactions + 142 +
+
+ Total Liquidity + ${tvlFormatted} +
+
+
+ + + + + + IL Risk + + + +

+ This pool's tokens have moderate price correlation. Estimated + impermanent loss at ±25% price divergence is -5.7%. Consider + concentrated ranges carefully. +

+
+
+
+ +
+ + + Recent Transactions + + +
+ + + + + + + + + + + + {PLACEHOLDER_TXS.map((tx, i) => ( + + + + + + + + ))} + +
TypeAmount inAmount outAccountTime
+ {tx.type} + {tx.in}{tx.out}{tx.account}{tx.time}
+
+
+
+ + + + Price & TVL +
+ {(['1D', '1W', '1M', 'ALL'] as const).map((r) => ( + + ))} +
+
+ +
+ {[40, 65, 45, 80, 55, 70, 50].map((h, i) => ( +
+ ))} +
+ + +
+
+
+
+ + {isSuccessModalOpen && ( + setIsSuccessModalOpen(false)} + swapResult={txResult} + lpDetails={lpDetails} + orderStatus={ + txResult?.noteId ? orderStatus[txResult.noteId]?.status : undefined + } + /> + )} +
+ ); +} + From 62ca64be3362d577aa63bdd848eacc185d75fa39 Mon Sep 17 00:00:00 2001 From: mico Date: Mon, 2 Mar 2026 19:30:29 +0100 Subject: [PATCH 03/49] para wallet update, autocomplete on swap, trading view on detail view --- package-lock.json | 359 ++++++++++++++------- package.json | 9 +- src/components/Header.tsx | 10 +- src/components/TokenAutocomplete.tsx | 146 +++++++++ src/components/TradingViewCandlesChart.tsx | 177 ++++++++++ src/mocks/poolDetailMocks.ts | 160 +++++++++ src/pages/LiquidityPools.tsx | 5 +- src/pages/PoolDetail.tsx | 44 +-- src/pages/Swap.tsx | 52 +-- 9 files changed, 768 insertions(+), 194 deletions(-) create mode 100644 src/components/TokenAutocomplete.tsx create mode 100644 src/components/TradingViewCandlesChart.tsx create mode 100644 src/mocks/poolDetailMocks.ts diff --git a/package-lock.json b/package-lock.json index ae74818..b694302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { - "name": "vite-project", - "version": "0.3.0", + "name": "zoroswap-frontend", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "vite-project", - "version": "0.3.0", + "name": "zoroswap-frontend", + "version": "0.5.0", "hasInstallScript": true, "dependencies": { "@demox-labs/miden-wallet-adapter": "0.10.0", - "@getpara/react-sdk-lite": "^2.2.0", + "@getpara/react-sdk-lite": "^2.13.0", "@miden-sdk/miden-para": "^0.13.0", "@miden-sdk/miden-sdk": "^0.13.0", "@miden-sdk/react": "^0.13.2", @@ -22,6 +22,7 @@ "async-mutex": "^0.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lightweight-charts": "^5.1.0", "lucide-react": "^0.556.0", "react": "^19.2.1", "react-dom": "^19.2.1", @@ -110,6 +111,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -652,6 +654,7 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", + "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -1827,9 +1830,9 @@ "license": "MIT" }, "node_modules/@getpara/core-components": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@getpara/core-components/-/core-components-2.8.0.tgz", - "integrity": "sha512-ec3BM4xfahgu4/gAep4sitJZYEkne8vxW1TMVhvcZYimp7g52Xw18tLuF3v45aKPD/JDZgD8Z+3cqvwmyluMxA==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@getpara/core-components/-/core-components-2.13.0.tgz", + "integrity": "sha512-zzSt8KyLkEP4PD92DQ9GkxR4uiNZNWad6NR2SkOtnjQYIyX7oaLppqbiXyywBiOUjE6uNjvpDcAc3J36URCIsQ==", "dependencies": { "@stencil/core": "^4.7.0", "color-blend": "^4.0.0", @@ -1840,28 +1843,29 @@ } }, "node_modules/@getpara/core-sdk": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@getpara/core-sdk/-/core-sdk-2.8.0.tgz", - "integrity": "sha512-zJKl3feRVvZyTMyPDIyxrIAK0kWKFTlQMqLMabTi/gqHWEteXLzJU2WVdeOwIyjxsH5CT5XL4ivuUSL7HAmmcw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@getpara/core-sdk/-/core-sdk-2.13.0.tgz", + "integrity": "sha512-rhJR/4z4HojALVD40Lsxr3W0DBKrcK2jiub4meetQ/3Vze84oJQnBCgdPDkBNxDIP1LH1H6PUEpYcmWogS4ipQ==", "dependencies": { "@celo/utils": "^8.0.2", "@cosmjs/encoding": "^0.32.4", "@ethereumjs/util": "^9.1.0", - "@getpara/user-management-client": "2.8.0", + "@getpara/user-management-client": "2.13.0", "@noble/hashes": "^1.5.0", "base64url": "^3.0.1", "libphonenumber-js": "^1.11.7", "node-forge": "^1.3.1", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "xstate": "^5.24.0" } }, "node_modules/@getpara/react-common": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@getpara/react-common/-/react-common-2.8.0.tgz", - "integrity": "sha512-qBAc2P4ChLZOlvx4YlAkwyQaOche6hMMFlyW+kFQslWPDoH6b+BzFBwuX1/V+xVuB68mgO3tAFmeZQ8KRs9SLQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@getpara/react-common/-/react-common-2.13.0.tgz", + "integrity": "sha512-6BdPKMXm+l+U3KT8+w2Xi8gc4v0pOWsxRAwu5bwRhiYdWV7vm30+DaUoVtnf5SPADnaa/vJd1CeR7AWnCJSbmg==", "dependencies": { - "@getpara/react-components": "2.8.0", - "@getpara/web-sdk": "2.8.0", + "@getpara/react-components": "2.13.0", + "@getpara/web-sdk": "2.13.0", "@moonpay/moonpay-react": "^1.10.6", "@ramp-network/ramp-instant-sdk": "^4.0.5", "libphonenumber-js": "^1.11.7", @@ -1874,24 +1878,25 @@ } }, "node_modules/@getpara/react-components": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@getpara/react-components/-/react-components-2.8.0.tgz", - "integrity": "sha512-xjFquu/mmNszeRqoINm7KEh8nIl91BUsAcM8NfHUjfct+FjgGJCOd2TY3ADZj2CwCUZ9z9kM3Zpdx1HEGPZzkg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@getpara/react-components/-/react-components-2.13.0.tgz", + "integrity": "sha512-vm3LbLtSG+aeYqN4OKKqd4u9ShDqCj65vX0q9J2iLd/YUI12DnOZwOejfoDsOzrvV7LMeyqEO5FGnSReP04ezg==", "dependencies": { - "@getpara/core-components": "2.8.0" + "@getpara/core-components": "2.13.0" } }, "node_modules/@getpara/react-sdk-lite": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@getpara/react-sdk-lite/-/react-sdk-lite-2.8.0.tgz", - "integrity": "sha512-OG8oT3D4hpaSLMrjk0uRkt7EQ9Xfl5teG8TsTxS9F3ASG8Nc4MWHw7qSinK5JY+FQ8FrGlsKvpyzp6HjvdcOfw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@getpara/react-sdk-lite/-/react-sdk-lite-2.13.0.tgz", + "integrity": "sha512-68nT4bnzga7ZzeKehoo4cO6ydNEDN0C/c2cR5D0JT/w2CBnnYwHrwo5UjR1SWG+EiR/pxdu+ASFfgmfe3d8knw==", "dependencies": { - "@getpara/react-common": "2.8.0", - "@getpara/react-components": "2.8.0", - "@getpara/web-sdk": "2.8.0", + "@getpara/react-common": "2.13.0", + "@getpara/react-components": "2.13.0", + "@getpara/web-sdk": "2.13.0", "date-fns": "^3.6.0", "framer-motion": "^11.3.31", "libphonenumber-js": "^1.11.7", + "socket.io-client": "^4.5.1", "styled-components": "^6.1.8", "zustand": "^4.5.2", "zustand-sync-tabs": "^0.2.2" @@ -1934,27 +1939,28 @@ } }, "node_modules/@getpara/shared": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@getpara/shared/-/shared-1.9.0.tgz", - "integrity": "sha512-HiW+4tpACqhZeg2VjR428uGm9YMV1/CBh+NFemabKj+3B+fdOcI3y+7/MAhZMT7sa/7PemBdUNOJnc+Tbcxifg==" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@getpara/shared/-/shared-1.10.0.tgz", + "integrity": "sha512-xSBzCrQEbm+p32aoIjZcKL+TrfNRRYvuwGQrzvEMLB+vUxvkPMWz2n1uifRKzIVb/1MtIX4lDv5WiNe8xqFS9A==" }, "node_modules/@getpara/user-management-client": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@getpara/user-management-client/-/user-management-client-2.8.0.tgz", - "integrity": "sha512-b86cDR9PiwEyBGLLUrZnb98M1QKI5Qc35dJfubEYjhuUzhYYgV3qCYkkNPu+9SFxbCE1Jvu65v8M6dPD9E+LyQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@getpara/user-management-client/-/user-management-client-2.13.0.tgz", + "integrity": "sha512-2yGNjC8mgOoM1lfzjrYoK6VjvlzBQrwk0XUHKz0nSGeWvfsgQPuEU2jeZqf0cqj9jg+FbXnhuxdJU8aQaTgx8g==", "dependencies": { - "@getpara/shared": "1.9.0", + "@getpara/shared": "1.10.0", "axios": "^1.8.4", "libphonenumber-js": "^1.11.7" } }, "node_modules/@getpara/web-sdk": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@getpara/web-sdk/-/web-sdk-2.8.0.tgz", - "integrity": "sha512-8czkLtT+yn7nDALDt4lCt43NbC7wHK5lEYf0GHcOWpf+sa45R7o9ObCrsA0O70Gj42a3atvzf2b4GFkRDfJ09w==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@getpara/web-sdk/-/web-sdk-2.13.0.tgz", + "integrity": "sha512-n5pqA7Lz41npksZFwulV3DSRTdZJr75M3AyvT8eSWYZB9+lOtfgKA1x6+MEHNxcQa/diyexguvmH88KmHnXI0w==", + "peer": true, "dependencies": { - "@getpara/core-sdk": "2.8.0", - "@getpara/user-management-client": "2.8.0", + "@getpara/core-sdk": "2.13.0", + "@getpara/user-management-client": "2.13.0", "base64url": "^3.0.1", "buffer": "6.0.3", "cbor-web": "^9.0.2", @@ -2149,6 +2155,7 @@ "version": "0.13.0", "resolved": "https://registry.npmjs.org/@miden-sdk/miden-sdk/-/miden-sdk-0.13.0.tgz", "integrity": "sha512-N0qUCZW9Dvk3Oqj37IrGmm0b0v3Nq5qHsX3BtQIzZIwDXKXKPBxy/0lO40oCwDtwI8AfriZQyMLbJR81Fo4Vpg==", + "peer": true, "dependencies": { "@rollup/plugin-typescript": "^12.3.0", "dexie": "^4.0.1", @@ -2160,6 +2167,7 @@ "resolved": "https://registry.npmjs.org/@miden-sdk/react/-/react-0.13.2.tgz", "integrity": "sha512-78i3/5YUUwitqvJA02HVXZ61RXEOyaiRsm1agVUtOblOmazuIgMEW4P3gdlX9YUiGr06l9O+Rw1ol5FZCOJTXQ==", "license": "MIT", + "peer": true, "dependencies": { "zustand": "^5.0.0" }, @@ -2999,7 +3007,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3013,7 +3020,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3021,9 +3027,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", "cpu": [ "arm64" ], @@ -3034,9 +3040,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", "cpu": [ "x64" ], @@ -3053,7 +3059,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3067,7 +3072,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3081,7 +3085,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3095,7 +3098,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3103,9 +3105,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", "cpu": [ "arm64" ], @@ -3116,9 +3118,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", "cpu": [ "arm64" ], @@ -3135,7 +3137,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3149,7 +3150,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3163,7 +3163,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3177,7 +3176,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3191,7 +3189,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3205,7 +3202,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3219,7 +3215,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3227,9 +3222,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", "cpu": [ "x64" ], @@ -3240,9 +3235,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", "cpu": [ "x64" ], @@ -3259,7 +3254,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3273,7 +3267,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3281,9 +3274,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", "cpu": [ "arm64" ], @@ -3300,7 +3293,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3314,7 +3306,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3322,9 +3313,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", "cpu": [ "x64" ], @@ -3406,10 +3397,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@stencil/core": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.42.0.tgz", - "integrity": "sha512-1mMkQ3+5jE343detvyFK+U3tJM8Qp8aaaOnGMo815BKMnFShUpioF9ziaX0dJ+goDKWIWbGZ2438dHIGsvE5ug==", + "version": "4.43.2", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.43.2.tgz", + "integrity": "sha512-hOprMw/n1fyu5OOtZG7N8sqiKxPshR1oss/y1qr6r98cVV9NcVoCMz2x2TT2enkHan6pCMpiQgUU7vmN90lIVw==", "license": "MIT", "bin": { "stencil": "bin/stencil" @@ -3419,14 +3416,14 @@ "npm": ">=7.10.0" }, "optionalDependencies": { - "@rollup/rollup-darwin-arm64": "4.34.9", - "@rollup/rollup-darwin-x64": "4.34.9", - "@rollup/rollup-linux-arm64-gnu": "4.34.9", - "@rollup/rollup-linux-arm64-musl": "4.34.9", - "@rollup/rollup-linux-x64-gnu": "4.34.9", - "@rollup/rollup-linux-x64-musl": "4.34.9", - "@rollup/rollup-win32-arm64-msvc": "4.34.9", - "@rollup/rollup-win32-x64-msvc": "4.34.9" + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0" } }, "node_modules/@tanstack/eslint-plugin-query": { @@ -3467,6 +3464,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.20" }, @@ -3602,8 +3600,8 @@ "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3612,8 +3610,8 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3687,6 +3685,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -3962,6 +3961,7 @@ "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.3.2.tgz", "integrity": "sha512-e4aefdzEki657u7P6miuBijp0WKmtSsuY2/NT9e3zfJxr+QX5Edr5EcFF0Cg5OMMQ1y32x+g8ogMDppD9aX3kw==", "license": "MIT", + "peer": true, "dependencies": { "eventemitter3": "5.0.1", "mipd": "0.0.7", @@ -4050,6 +4050,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4444,13 +4445,13 @@ } }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -4854,6 +4855,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5588,7 +5590,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5758,7 +5759,8 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1551306.tgz", "integrity": "sha512-CFx8QdSim8iIv+2ZcEOclBKTQY6BI1IEDa7Tm9YkwAXzEWFndTEzpTo5jAUhSnq24IC7xaDw0wvGcm96+Y3PEg==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dexie": { "version": "4.3.0", @@ -5895,6 +5897,49 @@ "once": "^1.4.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -6181,6 +6226,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6637,6 +6683,12 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fancy-canvas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6892,7 +6944,8 @@ "version": "2.16.9", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.9.tgz", "integrity": "sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fraction.js": { "version": "5.3.4", @@ -6939,7 +6992,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8113,6 +8165,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8238,11 +8291,20 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.36.tgz", - "integrity": "sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==", + "version": "1.12.38", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.38.tgz", + "integrity": "sha512-vwzxmasAy9hZigxtqTbFEwp8ZdZ975TiqVDwj5bKx5sR+zi5ucUQy9mbVTkKM9GzqdLdxux/hTw2nmN5J7POMA==", "license": "MIT" }, + "node_modules/lightweight-charts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-5.1.0.tgz", + "integrity": "sha512-jEAYR4ODYeyNZcWUigsoLTl52rbPmgXnvd5FLIv/ZoA/2sSDw63YKnef8n4yhzum7W926yHeFwlm7ididKb7YQ==", + "license": "Apache-2.0", + "dependencies": { + "fancy-canvas": "2.1.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -9399,7 +9461,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -10082,6 +10143,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10161,6 +10223,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10747,6 +10810,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10756,6 +10820,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11145,8 +11210,9 @@ "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -11193,7 +11259,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11207,7 +11272,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11221,7 +11285,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11235,7 +11298,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11249,7 +11311,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11263,7 +11324,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11277,7 +11337,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11291,7 +11350,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11631,6 +11689,34 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -11962,9 +12048,9 @@ } }, "node_modules/styled-components": { - "version": "6.3.8", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.8.tgz", - "integrity": "sha512-Kq/W41AKQloOqKM39zfaMdJ4BcYDw/N5CIq4/GTI0YjU6pKcZ1KKhk6b4du0a+6RA9pIfOP/eu94Ge7cu+PDCA==", + "version": "6.3.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.11.tgz", + "integrity": "sha512-opzgceGlQ5rdZdGwf9ddLW7EM2F4L7tgsgLn6fFzQ2JgE5EVQ4HZwNkcgB1p8WfOBx1GEZP3fa66ajJmtXhSrA==", "license": "MIT", "dependencies": { "@emotion/is-prop-valid": "1.4.0", @@ -12110,6 +12196,7 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -12405,8 +12492,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12460,9 +12547,9 @@ "license": "MIT" }, "node_modules/ua-parser-js": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.8.tgz", - "integrity": "sha512-BdnBM5waFormdrOFBU+cA90R689V0tWUWlIG2i30UXxElHjuCu5+dOV2Etw3547jcQ/yaLtPm9wrqIuOY2bSJg==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.9.tgz", + "integrity": "sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==", "funding": [ { "type": "opencollective", @@ -12739,6 +12826,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", @@ -12836,6 +12924,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13238,8 +13327,8 @@ "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -13256,6 +13345,24 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xstate": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.28.0.tgz", + "integrity": "sha512-Iaqq6ZrUzqeUtA3hC5LQKZfR8ZLzEFTImMHJM3jWEdVvXWdKvvVLXZEiNQWm3SCA9ZbEou/n5rcsna1wb9t28A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -13385,8 +13492,9 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -13409,6 +13517,7 @@ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12.20.0" }, diff --git a/package.json b/package.json index 4a033fe..cac2b62 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "vite-project", + "name": "zoroswap-frontend", "private": true, - "version": "0.3.0", + "version": "0.5.0", "type": "module", "scripts": { "dev": "vite", @@ -12,7 +12,7 @@ }, "dependencies": { "@demox-labs/miden-wallet-adapter": "0.10.0", - "@getpara/react-sdk-lite": "^2.2.0", + "@getpara/react-sdk-lite": "^2.13.0", "@miden-sdk/miden-para": "^0.13.0", "@miden-sdk/miden-sdk": "^0.13.0", "@miden-sdk/react": "^0.13.2", @@ -24,6 +24,7 @@ "async-mutex": "^0.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lightweight-charts": "^5.1.0", "lucide-react": "^0.556.0", "react": "^19.2.1", "react-dom": "^19.2.1", @@ -35,7 +36,7 @@ "wagmi": "^3.1.0" }, "overrides": { - "@getpara/web-sdk": "2.8.0" + "@getpara/web-sdk": "2.13.0" }, "devDependencies": { "@eslint/css": "^0.14.1", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9b74080..1d6518c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -13,8 +13,9 @@ export function Header() { }`; return ( -
- +
+
+ Zoro -
); } diff --git a/src/components/TokenAutocomplete.tsx b/src/components/TokenAutocomplete.tsx new file mode 100644 index 0000000..473303c --- /dev/null +++ b/src/components/TokenAutocomplete.tsx @@ -0,0 +1,146 @@ +import AssetIcon from '@/components/AssetIcon'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { cn } from '@/lib/utils'; +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { ChevronDown, Search } from 'lucide-react'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +export function TokenAutocomplete({ + tokens, + value, + onChange, + disabled, + excludeFaucetIdBech32, + placeholder = 'Select token', + className, +}: { + tokens: TokenConfig[]; + value?: TokenConfig; + onChange: (faucetIdBech32: string) => void; + disabled?: boolean; + excludeFaucetIdBech32?: string; + placeholder?: string; + className?: string; +}) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const [activeIndex, setActiveIndex] = useState(0); + const inputRef = useRef(null); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + const list = tokens.filter(t => t.faucetIdBech32 !== excludeFaucetIdBech32); + if (!q) return list; + return list.filter(t => + t.symbol.toLowerCase().includes(q) || t.name.toLowerCase().includes(q), + ); + }, [excludeFaucetIdBech32, query, tokens]); + + useEffect(() => { + if (!open) return; + setQuery(''); + setActiveIndex(0); + // Let Radix mount content first + const t = window.setTimeout(() => inputRef.current?.focus(), 0); + return () => window.clearTimeout(t); + }, [open]); + + useEffect(() => { + if (activeIndex >= filtered.length) setActiveIndex(0); + }, [activeIndex, filtered.length]); + + return ( + + + + + e.preventDefault()} + > +
+ + setQuery(e.target.value)} + placeholder='Search token...' + className='pl-9 rounded-lg bg-muted/40 border-muted-foreground/20' + onKeyDown={(e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setActiveIndex(i => Math.min(filtered.length - 1, i + 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setActiveIndex(i => Math.max(0, i - 1)); + } else if (e.key === 'Enter') { + e.preventDefault(); + const item = filtered[activeIndex]; + if (!item) return; + onChange(item.faucetIdBech32); + setOpen(false); + } else if (e.key === 'Escape') { + setOpen(false); + } + }} + /> +
+ +
+ {filtered.length === 0 && ( +
+ No tokens found +
+ )} + {filtered.map((t, idx) => ( + setActiveIndex(idx)} + onSelect={() => { + onChange(t.faucetIdBech32); + setOpen(false); + }} + > + +
+ {t.symbol} + + {t.name} + +
+
+ ))} +
+
+
+ ); +} + diff --git a/src/components/TradingViewCandlesChart.tsx b/src/components/TradingViewCandlesChart.tsx new file mode 100644 index 0000000..25a58cf --- /dev/null +++ b/src/components/TradingViewCandlesChart.tsx @@ -0,0 +1,177 @@ +import { ThemeContext } from '@/providers/ThemeContext'; +import { + CandlestickSeries, + ColorType, + CrosshairMode, + HistogramSeries, + type IChartApi, + type ISeriesApi, + type UTCTimestamp, + createChart, +} from 'lightweight-charts'; +import { useContext, useEffect, useMemo, useRef } from 'react'; + +export type TradingViewCandle = { + time: UTCTimestamp; + open: number; + high: number; + low: number; + close: number; + volume?: number; +}; + +export function TradingViewCandlesChart({ + candles, + height = 256, + className, +}: { + candles: TradingViewCandle[]; + height?: number; + className?: string; +}) { + const { theme } = useContext(ThemeContext); + const rootRef = useRef(null); + const chartRef = useRef(null); + const candleSeriesRef = useRef | null>(null); + const volumeSeriesRef = useRef | null>(null); + + const isDark = useMemo(() => { + if (theme === 'dark') return true; + if (theme === 'light') return false; + return document.documentElement.classList.contains('dark'); + }, [theme]); + + useEffect(() => { + const el = rootRef.current; + if (!el) return; + + const bg = isDark ? '#131722' : '#ffffff'; + const fg = isDark ? '#d1d4dc' : '#1f2937'; + const grid = isDark ? 'rgba(42, 46, 57, 0.6)' : 'rgba(229, 231, 235, 0.8)'; + const border = isDark ? 'rgba(42, 46, 57, 0.9)' : 'rgba(229, 231, 235, 1)'; + + const chart = createChart(el, { + height, + layout: { + background: { type: ColorType.Solid, color: bg }, + textColor: fg, + fontFamily: 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial', + fontSize: 12, + }, + grid: { + vertLines: { color: grid }, + horzLines: { color: grid }, + }, + rightPriceScale: { + borderColor: border, + }, + timeScale: { + borderColor: border, + timeVisible: true, + secondsVisible: false, + }, + crosshair: { + mode: CrosshairMode.Magnet, + vertLine: { + color: isDark ? 'rgba(122, 146, 202, 0.45)' : 'rgba(37, 99, 235, 0.25)', + width: 1, + style: 0, + labelBackgroundColor: isDark ? '#1f2a44' : '#2563eb', + }, + horzLine: { + color: isDark ? 'rgba(122, 146, 202, 0.45)' : 'rgba(37, 99, 235, 0.25)', + width: 1, + style: 0, + labelBackgroundColor: isDark ? '#1f2a44' : '#2563eb', + }, + }, + handleScroll: true, + handleScale: true, + }); + + const candleSeries = chart.addSeries(CandlestickSeries, { + upColor: isDark ? '#22c55e' : '#16a34a', + downColor: isDark ? '#ef4444' : '#dc2626', + borderUpColor: isDark ? '#22c55e' : '#16a34a', + borderDownColor: isDark ? '#ef4444' : '#dc2626', + wickUpColor: isDark ? '#22c55e' : '#16a34a', + wickDownColor: isDark ? '#ef4444' : '#dc2626', + }); + + const volumeSeries = chart.addSeries(HistogramSeries, { + priceFormat: { type: 'volume' }, + priceScaleId: '', + color: isDark ? 'rgba(78, 140, 255, 0.35)' : 'rgba(37, 99, 235, 0.25)', + base: 0, + }); + + volumeSeries.priceScale().applyOptions({ + scaleMargins: { top: 0.8, bottom: 0 }, + }); + + chartRef.current = chart; + candleSeriesRef.current = candleSeries; + volumeSeriesRef.current = volumeSeries; + + const resize = () => { + if (!rootRef.current || !chartRef.current) return; + const w = rootRef.current.clientWidth; + chartRef.current.applyOptions({ width: w, height }); + }; + resize(); + + const ro = new ResizeObserver(() => resize()); + ro.observe(el); + + return () => { + ro.disconnect(); + chart.remove(); + chartRef.current = null; + candleSeriesRef.current = null; + volumeSeriesRef.current = null; + }; + }, [height, isDark]); + + useEffect(() => { + if (!candleSeriesRef.current || !volumeSeriesRef.current) return; + if (!candles?.length) { + candleSeriesRef.current.setData([]); + volumeSeriesRef.current.setData([]); + return; + } + + candleSeriesRef.current.setData( + candles.map(c => ({ + time: c.time, + open: c.open, + high: c.high, + low: c.low, + close: c.close, + })), + ); + + volumeSeriesRef.current.setData( + candles.map(c => { + const up = c.close >= c.open; + return { + time: c.time, + value: c.volume ?? 0, + color: up + ? (isDark ? 'rgba(34,197,94,0.35)' : 'rgba(22,163,74,0.25)') + : (isDark ? 'rgba(239,68,68,0.35)' : 'rgba(220,38,38,0.25)'), + }; + }), + ); + + chartRef.current?.timeScale().fitContent(); + }, [candles, isDark]); + + return ( +
+ ); +} + diff --git a/src/mocks/poolDetailMocks.ts b/src/mocks/poolDetailMocks.ts new file mode 100644 index 0000000..f3d6a7b --- /dev/null +++ b/src/mocks/poolDetailMocks.ts @@ -0,0 +1,160 @@ +import type { UTCTimestamp } from 'lightweight-charts'; + +export type MockCandle = { + time: UTCTimestamp; + open: number; + high: number; + low: number; + close: number; + volume: number; +}; + +export type MockRecentTx = { + type: 'Swap' | 'Add' | 'Remove'; + amountIn: string; + amountOut: string; + account: string; + timeAgo: string; +}; + +type Range = '1D' | '1W' | '1M' | 'ALL'; + +function hashStringToSeed(input: string) { + // Deterministic, small, and good enough for mock data. + let h = 2166136261; + for (let i = 0; i < input.length; i++) { + h ^= input.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return h >>> 0; +} + +function mulberry32(seed: number) { + return () => { + let t = seed += 0x6D2B79F5; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +function clamp(n: number, min: number, max: number) { + return Math.max(min, Math.min(max, n)); +} + +function formatCompact(n: number) { + const abs = Math.abs(n); + if (abs >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`; + if (abs >= 1_000) return `${(n / 1_000).toFixed(2)}K`; + return n.toFixed(2); +} + +function getRangeSpec(range: Range) { + switch (range) { + case '1D': + return { intervalSec: 5 * 60, points: 24 * 12 }; + case '1W': + return { intervalSec: 60 * 60, points: 24 * 7 }; + case '1M': + return { intervalSec: 4 * 60 * 60, points: 30 * 6 }; + case 'ALL': + default: + return { intervalSec: 24 * 60 * 60, points: 365 }; + } +} + +export function getMockPoolCandles({ + seedKey, + range, +}: { + seedKey: string; + range: Range; +}): MockCandle[] { + const seed = hashStringToSeed(`${seedKey}:${range}`); + const rand = mulberry32(seed); + + const { intervalSec, points } = getRangeSpec(range); + const nowSec = Math.floor(Date.now() / 1000); + const end = nowSec - (nowSec % intervalSec); + const start = end - intervalSec * points; + + // Pick a stable-ish base so different pools “feel” different. + const base = 0.5 + rand() * 250; + let prevClose = base * (0.95 + rand() * 0.1); + + const candles: MockCandle[] = []; + for (let i = 0; i < points; i++) { + const t = start + i * intervalSec; + + // Gentle drift + noise + occasional impulse to mimic TV candles. + const drift = (rand() - 0.5) * 0.0012; + const noise = (rand() - 0.5) * 0.006; + const impulse = rand() < 0.03 ? (rand() - 0.5) * 0.05 : 0; + const change = clamp(drift + noise + impulse, -0.08, 0.08); + + const open = prevClose; + const close = Math.max(0.0001, open * (1 + change)); + const wick = 0.002 + rand() * 0.02; + const high = Math.max(open, close) * (1 + wick * (0.4 + rand())); + const low = Math.min(open, close) * (1 - wick * (0.4 + rand())); + + const volBase = 300 + rand() * 4000; + const vol = volBase * (1 + Math.min(2.5, Math.abs(change) * 25)); + + candles.push({ + time: t as UTCTimestamp, + open: Number(open.toFixed(6)), + high: Number(high.toFixed(6)), + low: Number(low.toFixed(6)), + close: Number(close.toFixed(6)), + volume: Math.round(vol), + }); + + prevClose = close; + } + + return candles; +} + +export function getMockRecentTransactions({ + seedKey, + baseSymbol, +}: { + seedKey: string; + baseSymbol: string; +}): MockRecentTx[] { + const seed = hashStringToSeed(`tx:${seedKey}`); + const rand = mulberry32(seed); + + const mkAcct = () => { + const hex = Array.from({ length: 8 }, () => Math.floor(rand() * 16).toString(16)).join(''); + const hex2 = Array.from({ length: 8 }, () => Math.floor(rand() * 16).toString(16)).join(''); + return `0x${hex}...${hex2}`; + }; + + const ago = (mins: number) => mins < 60 ? `${mins} min ago` : `${Math.round(mins / 60)} hr ago`; + + const types: MockRecentTx['type'][] = ['Swap', 'Add', 'Remove', 'Swap', 'Swap']; + return types.map((type, i) => { + const mins = 2 + i * (5 + Math.floor(rand() * 7)); + const amountA = 0.05 + rand() * 4; + const price = 0.5 + rand() * 250; + const amountB = amountA * price; + + const inStr = type === 'Swap' + ? `${formatCompact(amountA)} ${baseSymbol}` + : `${formatCompact(amountA)} ${baseSymbol}`; + const outStr = type === 'Swap' + ? `${formatCompact(amountB)} USDC` + : `${formatCompact(amountB)} USDC`; + + return { + type, + amountIn: inStr, + amountOut: outStr, + account: mkAcct(), + timeAgo: ago(mins), + }; + }); +} + diff --git a/src/pages/LiquidityPools.tsx b/src/pages/LiquidityPools.tsx index 2a73e77..e985a98 100644 --- a/src/pages/LiquidityPools.tsx +++ b/src/pages/LiquidityPools.tsx @@ -90,8 +90,9 @@ function LiquidityPools() { ); const userPositions = useMemo(() => { - if (!poolsInfo?.liquidityPools || !poolBalances) return []; - return poolsInfo.liquidityPools + const liquidityPools = poolsInfo?.liquidityPools; + if (!liquidityPools || !poolBalances) return []; + return liquidityPools .filter((pool) => pool.poolType === 'hfAMM') .map((pool) => { const balance = poolBalances.find((b) => b.faucetIdBech32 === pool.faucetIdBech32); diff --git a/src/pages/PoolDetail.tsx b/src/pages/PoolDetail.tsx index 7e72149..851c8eb 100644 --- a/src/pages/PoolDetail.tsx +++ b/src/pages/PoolDetail.tsx @@ -23,18 +23,12 @@ import { Link, useParams } from 'react-router-dom'; import AssetIcon from '@/components/AssetIcon'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { TradingViewCandlesChart } from '@/components/TradingViewCandlesChart'; +import { getMockPoolCandles, getMockRecentTransactions } from '@/mocks/poolDetailMocks'; const feeTierForSymbol = (symbol: string) => /USDC|USDT|DAI|BUSD/i.test(symbol) ? '0.01%' : '0.30%'; -const PLACEHOLDER_TXS = [ - { type: 'Swap' as const, in: '0.5 ETH', out: '1,090 USDC', account: '0x1a2b...3c4d', time: '2 min ago' }, - { type: 'Add' as const, in: '1.0 ETH', out: '2,180 USDC', account: '0x5e6f...7a8b', time: '8 min ago' }, - { type: 'Remove' as const, in: '0.25 ETH', out: '545 USDC', account: '0x9c0d...1e2f', time: '14 min ago' }, - { type: 'Swap' as const, in: '2,500 USDC', out: '1.14 ETH', account: '0x3a4b...5c6d', time: '22 min ago' }, - { type: 'Swap' as const, in: '0.1 ETH', out: '218 USDC', account: '0x7e8f...9a0b', time: '31 min ago' }, -]; - const TYPE_COLORS: Record = { Swap: 'text-primary', Add: 'text-green-600', @@ -131,6 +125,19 @@ export default function PoolDetail() { expo: decimals, }); const pairLabel = `${pool.symbol} / USDC`; + const mockCandles = useMemo(() => { + return getMockPoolCandles({ + seedKey: pool.faucetIdBech32, + range: chartRange, + }); + }, [pool.faucetIdBech32, chartRange]); + + const mockRecentTxs = useMemo(() => { + return getMockRecentTransactions({ + seedKey: pool.faucetIdBech32, + baseSymbol: pool.symbol, + }); + }, [pool.faucetIdBech32, pool.symbol]); return (
@@ -323,15 +330,15 @@ export default function PoolDetail() { - {PLACEHOLDER_TXS.map((tx, i) => ( + {mockRecentTxs.map((tx, i) => ( {tx.type} - {tx.in} - {tx.out} + {tx.amountIn} + {tx.amountOut} {tx.account} - {tx.time} + {tx.timeAgo} ))} @@ -358,14 +365,11 @@ export default function PoolDetail() {
-
- {[40, 65, 45, 80, 55, 70, 50].map((h, i) => ( -
- ))} +
+
diff --git a/src/pages/Swap.tsx b/src/pages/Swap.tsx index 1af1c55..6d330cc 100644 --- a/src/pages/Swap.tsx +++ b/src/pages/Swap.tsx @@ -1,4 +1,3 @@ -import AssetIcon from '@/components/AssetIcon'; import ExchangeRatio from '@/components/ExchangeRatio'; import { Footer } from '@/components/Footer'; import { Header } from '@/components/Header'; @@ -8,6 +7,7 @@ import Price from '@/components/Price'; import Slippage from '@/components/Slippage'; import SwapInputBuy from '@/components/SwapInputBuy'; import SwapPairs from '@/components/SwapPairs'; +import { TokenAutocomplete } from '@/components/TokenAutocomplete'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; @@ -277,25 +277,12 @@ function Swap() { }`} />
-
- -
- + setAsset('sell', id)} + excludeFaucetIdBech32={selectedAssetBuy?.faucetIdBech32} + />
{sellInputError && ( @@ -356,25 +343,12 @@ function Swap() { assetSell={selectedAssetSell} />
-
- -
- + setAsset('buy', id)} + excludeFaucetIdBech32={selectedAssetSell?.faucetIdBech32} + />
From 0cefc53600e3ab33208f881f2ce7477d313baf6f Mon Sep 17 00:00:00 2001 From: mico Date: Mon, 2 Mar 2026 22:30:47 +0100 Subject: [PATCH 04/49] xyk pool creator --- src/App.tsx | 2 + src/components/CreatePoolWizard.tsx | 486 ++++++++++++++++++++++++ src/components/Header.tsx | 18 +- src/components/LiquidityPoolRow.tsx | 62 ++- src/components/LiquidityPoolsTable.tsx | 126 ++++-- src/components/OrderStatus.tsx | 24 +- src/components/PoolDetailView.tsx | 27 +- src/components/PoolModal.tsx | 27 +- src/components/PositionCard.tsx | 97 +++-- src/components/SelectPoolModal.tsx | 65 ++++ src/hooks/usePoolsBalances.tsx | 5 +- src/pages/LiquidityPools.tsx | 36 +- src/pages/PoolDetail.tsx | 135 ++++--- src/pages/Pools.tsx | 134 +++++++ src/providers/UnifiedWalletProvider.tsx | 26 +- 15 files changed, 1067 insertions(+), 203 deletions(-) create mode 100644 src/components/CreatePoolWizard.tsx create mode 100644 src/components/SelectPoolModal.tsx create mode 100644 src/pages/Pools.tsx diff --git a/src/App.tsx b/src/App.tsx index 796913c..49e444e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Bounce, ToastContainer } from 'react-toastify'; import LiquidityPools from './pages/LiquidityPools'; import PoolDetail from './pages/PoolDetail'; +import Pools from './pages/Pools'; import ModalProvider from './providers/ModalProvider'; import { ZoroProvider } from './providers/ZoroProvider'; import { ParaProviderWrapper } from './providers/ParaProviderWrapper'; @@ -30,6 +31,7 @@ function AppRouter() { } /> } /> } /> + } /> } /> diff --git a/src/components/CreatePoolWizard.tsx b/src/components/CreatePoolWizard.tsx new file mode 100644 index 0000000..4640e4a --- /dev/null +++ b/src/components/CreatePoolWizard.tsx @@ -0,0 +1,486 @@ +import type { AccountId } from '@miden-sdk/miden-sdk'; +import AssetIcon from '@/components/AssetIcon'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { ModalContext } from '@/providers/ModalContext'; +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { useBalance } from '@/hooks/useBalance'; +import { bech32ToAccountId } from '@/lib/utils'; +import { cn } from '@/lib/utils'; +import { ArrowRight, Check, ChevronLeft, X } from 'lucide-react'; +import { useCallback, useContext, useMemo, useState } from 'react'; + +const FAUCET_ID_MIN_LENGTH = 40; +const FAUCET_ID_MAX_LENGTH = 100; + +function validateFaucetId(value: string): { valid: boolean; error?: string } { + const v = value?.trim(); + if (!v) return { valid: false, error: 'Faucet ID is required' }; + if (v.length < FAUCET_ID_MIN_LENGTH) return { valid: false, error: 'Faucet ID is too short' }; + if (v.length > FAUCET_ID_MAX_LENGTH) return { valid: false, error: 'Faucet ID is too long' }; + try { + const id = bech32ToAccountId(v); + if (id == null) return { valid: false, error: 'Invalid faucet ID format' }; + return { valid: true }; + } catch { + return { valid: false, error: 'Must be a valid bech32 faucet ID (AccountId)' }; + } +} + +function isValidFaucetId(value: string): boolean { + return validateFaucetId(value).valid; +} + +function minimalTokenFromFaucetId( + faucetIdBech32: string, + symbol: string, +): TokenConfig | null { + const v = faucetIdBech32?.trim(); + if (!v) return null; + try { + const faucetId = bech32ToAccountId(v) as AccountId | undefined; + if (!faucetId) return null; + return { + symbol, + name: symbol, + decimals: 18, + faucetId, + faucetIdBech32: v, + oracleId: '', + }; + } catch { + return null; + } +} + +type Step = 1 | 2 | 3 | 4; + +const FEE_TIERS = [ + { bps: 1, label: '0.01%', hint: 'Best for stable pairs' }, + { bps: 5, label: '0.05%', hint: 'Best for stable pairs' }, + { bps: 30, label: '0.30%', hint: 'Best for most pairs' }, + { bps: 100, label: '1.00%', hint: 'Best for exotic pairs' }, +] as const; + +type CreatedPoolDraft = { + id: string; + type: 'xyk'; + tokenA: Pick; + tokenB: Pick; + feeBps: number; + createdAt: number; + status: 'draft'; +}; + +const STORAGE_KEY = 'zoro-created-pools'; + +export function readCreatedPools(): CreatedPoolDraft[] { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw) as CreatedPoolDraft[]; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function writeCreatedPools(pools: CreatedPoolDraft[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(pools)); +} + +export function clearCreatedPools(): void { + writeCreatedPools([]); +} + +export function CreatePoolWizard({ onCreated }: { onCreated?: () => void }) { + const { closeModal } = useContext(ModalContext); + + const [step, setStep] = useState(1); + const [baseFaucetId, setBaseFaucetId] = useState(''); + const [quoteFaucetId, setQuoteFaucetId] = useState(''); + const [feeBps, setFeeBps] = useState(30); + const [amountA, setAmountA] = useState(''); + const [amountB, setAmountB] = useState(''); + + const tokenA = useMemo( + () => minimalTokenFromFaucetId(baseFaucetId, 'Base'), + [baseFaucetId], + ); + const tokenB = useMemo( + () => minimalTokenFromFaucetId(quoteFaucetId, 'Quote'), + [quoteFaucetId], + ); + + const { balance: balanceA, formatted: formattedBalanceA } = useBalance({ token: tokenA ?? undefined }); + const { balance: balanceB, formatted: formattedBalanceB } = useBalance({ token: tokenB ?? undefined }); + + const baseValidation = useMemo(() => validateFaucetId(baseFaucetId), [baseFaucetId]); + const quoteValidation = useMemo(() => validateFaucetId(quoteFaucetId), [quoteFaucetId]); + const baseValid = baseValidation.valid; + const quoteValid = quoteValidation.valid; + const canContinueStep1 = Boolean( + baseValid && quoteValid && baseFaucetId.trim() !== quoteFaucetId.trim(), + ); + const canContinueStep2 = useMemo(() => { + const a = amountA.trim() && parseFloat(amountA) > 0; + const b = amountB.trim() && parseFloat(amountB) > 0; + return a && b; + }, [amountA, amountB]); + + const next = useCallback(() => { + if (step === 1 && !canContinueStep1) return; + if (step === 2 && !canContinueStep2) return; + setStep((s) => (s < 4 ? (s + 1) as Step : s)); + }, [step, canContinueStep1, canContinueStep2]); + + const back = useCallback(() => { + setStep((s) => (s > 1 ? (s - 1) as Step : 1)); + }, []); + + const setMaxA = useCallback(() => { + if (formattedBalanceA) setAmountA(formattedBalanceA); + }, [formattedBalanceA]); + const setMaxB = useCallback(() => { + if (formattedBalanceB) setAmountB(formattedBalanceB); + }, [formattedBalanceB]); + + const onCreate = useCallback(() => { + if (!tokenA || !tokenB) return; + const draft: CreatedPoolDraft = { + id: crypto.randomUUID(), + type: 'xyk', + tokenA: { symbol: tokenA.symbol, name: tokenA.name, faucetIdBech32: tokenA.faucetIdBech32 }, + tokenB: { symbol: tokenB.symbol, name: tokenB.name, faucetIdBech32: tokenB.faucetIdBech32 }, + feeBps, + createdAt: Date.now(), + status: 'draft', + }; + const existing = readCreatedPools(); + writeCreatedPools([draft, ...existing]); + setStep(4); + onCreated?.(); + }, [feeBps, onCreated, tokenA, tokenB]); + + const initialPricePerA = useMemo(() => { + const a = parseFloat(amountA) || 0; + const b = parseFloat(amountB) || 0; + if (a <= 0) return null; + return b / a; + }, [amountA, amountB]); + + // Step 4: Congratulations + if (step === 4) { + return ( +
+
+

+ Congratulations! +

+ +
+
+
+ +
+

Pool created successfully!

+

+ View your Pool at Address: (saved to Your pools) +

+
+
+ + +
+
+ ); + } + + const stepTitle = + step === 1 + ? 'Create a new Liquidity Pool' + : step === 2 + ? 'Deposit Tokens' + : 'Confirm your details'; + + return ( +
+
+ {step < 4 && ( + + )} +
+ {step < 4 && ( + + Step {step} of 3 + + )} +

+ {step === 1 ? ( + <> + Create a new Liquidity Pool + + ) : ( + stepTitle + )} +

+
+ +
+ + {/* Step 1: Base & quote faucet ids + fee tier */} + {step === 1 && ( +
+
+

Select pair

+

+ Enter the base and quote token faucet IDs (valid AccountId or bech32 address). +

+
+
+ setBaseFaucetId(e.target.value)} + className={cn( + 'rounded-xl bg-background border font-mono text-sm', + baseFaucetId.trim() && !baseValid && 'border-destructive', + )} + /> + {baseFaucetId.trim() && !baseValid && baseValidation.error && ( +

{baseValidation.error}

+ )} +
+ + + +
+ setQuoteFaucetId(e.target.value)} + className={cn( + 'rounded-xl bg-background border font-mono text-sm', + quoteFaucetId.trim() && !quoteValid && 'border-destructive', + )} + /> + {quoteFaucetId.trim() && !quoteValid && quoteValidation.error && ( +

{quoteValidation.error}

+ )} +
+
+ {canContinueStep1 && ( +

+ You are creating a new XYK Pool. Amounts in the next step will use these faucets. +

+ )} +
+ +
+

Fee tier

+

+ The amount earned providing liquidity. Choose an amount that suits your risk tolerance and strategy. +

+
+ {FEE_TIERS.map(({ bps, label, hint }) => ( + + ))} +
+
+
+ )} + + {/* Step 2: Deposit tokens */} + {step === 2 && tokenA && tokenB && ( +
+

Select your tokens

+
+
+ setAmountA(e.target.value)} + className="flex-1 min-w-0 text-lg border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 h-auto" + /> +
+ + + + {tokenA.symbol} +
+
+
+ Balance: {formattedBalanceA ?? '0.00'} {tokenA.symbol} + +
+
+
+ setAmountB(e.target.value)} + className="flex-1 min-w-0 text-lg border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 h-auto" + /> +
+ + + + {tokenB.symbol} +
+
+
+ Balance: {formattedBalanceB ?? '0.00'} {tokenB.symbol} + +
+
+
+ )} + + {/* Step 3: Confirm */} + {step === 3 && tokenA && tokenB && ( +
+
+
+ Pair + {tokenA.symbol}/{tokenB.symbol} +
+
+ Fee tier + {(feeBps / 100).toFixed(2)}% +
+
+ Min. Deposit + + {amountB && parseFloat(amountB) >= 0 ? `${parseFloat(amountB).toFixed(1)} ${tokenB.symbol}` : '—'} + +
+
+ Max. Deposit + + {amountA && parseFloat(amountA) >= 0 ? `${parseFloat(amountA).toFixed(2)} ${tokenA.symbol}` : '—'} + +
+
+ APY + 1.24% - 3.45% +
+
+ APR + 2.07% +
+
+
+ )} + + {/* Actions: primary first; Back only on steps 2–3 (step 1 has chevron only) */} +
+ {step < 3 ? ( + + ) : ( + + )} + {step > 1 && ( + + )} +
+
+ ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1d6518c..5d74051 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -7,7 +7,7 @@ export function Header() { const navLinkClass = (path: string) => `px-4 py-2 rounded-md text-sm font-medium transition-colors h-10 inline-flex items-center ${ - location.pathname === path + location.pathname === path || location.pathname.startsWith(path + '/') ? 'text-foreground' : 'text-muted-foreground hover:text-foreground' }`; @@ -15,24 +15,28 @@ export function Header() { return (
- + Zoro - - zoro swap -
); diff --git a/src/components/LiquidityPoolsTable.tsx b/src/components/LiquidityPoolsTable.tsx index 74475a2..f4278c7 100644 --- a/src/components/LiquidityPoolsTable.tsx +++ b/src/components/LiquidityPoolsTable.tsx @@ -71,7 +71,7 @@ const LiquidityPoolsTable = ({ variant='outline' size='sm' className={`rounded-lg ${poolFilter === 'hot' ? 'bg-primary/10 border-primary text-primary' : 'bg-muted/50 border-muted-foreground/20'}`} - onClick={() => setPoolFilter('hot')} + onClick={() => setPoolFilter((f) => (f === 'hot' ? 'all' : 'hot'))} > Hot @@ -80,7 +80,7 @@ const LiquidityPoolsTable = ({ variant='outline' size='sm' className={`rounded-lg ${poolFilter === 'new' ? 'bg-primary/10 border-primary text-primary' : 'bg-muted/50 border-muted-foreground/20'}`} - onClick={() => setPoolFilter('new')} + onClick={() => setPoolFilter((f) => (f === 'new' ? 'all' : 'new'))} > New @@ -89,7 +89,7 @@ const LiquidityPoolsTable = ({ variant='outline' size='sm' className={`rounded-lg ${poolFilter === 'stables' ? 'bg-primary/10 border-primary text-primary' : 'bg-muted/50 border-muted-foreground/20'}`} - onClick={() => setPoolFilter('stables')} + onClick={() => setPoolFilter((f) => (f === 'stables' ? 'all' : 'stables'))} > Stables diff --git a/src/components/OrderStatus.tsx b/src/components/OrderStatus.tsx index 9d55160..801469c 100644 --- a/src/components/OrderStatus.tsx +++ b/src/components/OrderStatus.tsx @@ -157,14 +157,14 @@ export function OrderStatus({ onClick={handleClose} /> -
+
diff --git a/src/components/PoolModal.tsx b/src/components/PoolModal.tsx index e66dff7..f1d9a30 100644 --- a/src/components/PoolModal.tsx +++ b/src/components/PoolModal.tsx @@ -1,7 +1,10 @@ import { useDeposit } from '@/hooks/useDeposit'; import { useWithdraw } from '@/hooks/useWithdraw'; +import { usePoolsBalances } from '@/hooks/usePoolsBalances'; +import { DEFAULT_SLIPPAGE } from '@/lib/config'; import { ZoroContext } from '@/providers/ZoroContext'; import type { TokenConfig } from '@/providers/ZoroProvider'; +import { useOraclePrices } from '@/providers/OracleContext'; import { NoteType } from '@miden-sdk/miden-sdk'; import { ChevronDown, Info, Loader, AlertTriangle, X } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; @@ -9,9 +12,10 @@ import { parseUnits } from 'viem'; import { useBalance } from '../hooks/useBalance'; import { type PoolInfo } from '../hooks/usePoolsInfo'; import { ModalContext } from '../providers/ModalContext'; -import { formatTokenAmount } from '../utils/format'; +import { formatTokenAmount, formatUsd } from '../utils/format'; import type { LpDetails, TxResult } from './OrderStatus'; import AssetIcon from './AssetIcon'; +import Slippage from './Slippage'; import { Button } from './ui/button'; import { Input } from './ui/input'; @@ -44,11 +48,21 @@ export default function PoolModal({ const modalContext = useContext(ModalContext); const { tokens } = useContext(ZoroContext); const [mode, setMode] = useState(initialMode); + const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE); const [rawValue, setRawValue] = useState(BigInt(0)); const [inputError, setInputError] = useState(undefined); const [inputValue, setInputValue] = useState(''); + const [depositPct, setDepositPct] = useState(100); const [withdrawPct, setWithdrawPct] = useState(100); + const { data: poolBalancesData } = usePoolsBalances(); + const poolBalance = useMemo( + () => + poolBalancesData?.find((b) => b.faucetIdBech32 === pool.faucetIdBech32) ?? + null, + [poolBalancesData, pool.faucetIdBech32], + ); + const token = useMemo( () => Object.values(tokens).find((t) => t.faucetIdBech32 === pool.faucetIdBech32), @@ -58,17 +72,42 @@ export default function PoolModal({ () => Object.values(tokens).find((t) => t.symbol === 'USDC'), [tokens], ); + /** For hfAMM: LP symbol is "z" + underlying (e.g. zETH → ETH). Use underlying token for deposit/withdraw. */ + const underlyingSymbol = pool.symbol.startsWith('z') ? pool.symbol.slice(1) : pool.symbol; + const underlyingToken = useMemo( + () => Object.values(tokens).find((t) => t.symbol === underlyingSymbol) ?? quoteToken ?? null, + [tokens, underlyingSymbol, quoteToken], + ); + const oracleIds = useMemo( + () => + [pool.oracleId, underlyingToken?.oracleId, quoteToken?.oracleId].filter( + (id): id is string => typeof id === 'string' && id.length > 0, + ), + [pool.oracleId, underlyingToken?.oracleId, quoteToken?.oracleId], + ); + const oraclePrices = useOraclePrices(oracleIds); const { balance: balanceToken, refetch: refetchBalanceToken } = useBalance({ token, }); const { balance: balanceQuote } = useBalance({ token: quoteToken ?? undefined }); + const { balance: balanceUnderlying } = useBalance({ token: underlyingToken ?? undefined }); + const isHfAmm = pool.poolType === 'hfAMM'; const balance = - mode === 'Withdraw' ? lpBalance ?? BigInt(0) : balanceToken ?? BigInt(0); - const decimals = pool.decimals; + mode === 'Withdraw' + ? lpBalance ?? BigInt(0) + : isHfAmm + ? (balanceUnderlying ?? balanceQuote ?? BigInt(0)) + : (balanceToken ?? BigInt(0)); + const decimals = + mode === 'Deposit' && isHfAmm + ? (underlyingToken?.decimals ?? quoteToken?.decimals ?? 6) + : pool.decimals; + const depositWithdrawToken = isHfAmm ? (underlyingToken ?? quoteToken ?? token) : token; const clearForm = useCallback(() => { setInputValue(''); setRawValue(BigInt(0)); + setDepositPct(100); setWithdrawPct(100); refetchBalanceToken().catch(console.error); refetchPoolInfo?.(); @@ -120,26 +159,6 @@ export default function PoolModal({ onSuccess, ]); - const writeDeposit = useCallback(async () => { - if (token == null) return; - await deposit({ - amount: rawValue, - minAmountOut: BigInt(1), - token, - noteType: NoteType.Public, - }); - }, [rawValue, deposit, token]); - - const writeWithdraw = useCallback(async () => { - if (token == null) return; - await withdraw({ - amount: rawValue, - minAmountOut: BigInt(1), - token, - noteType: NoteType.Public, - }); - }, [rawValue, withdraw, token]); - const setAmountPercentage = useCallback( (percentage: number) => { const newValue = (BigInt(percentage) * balance) / BigInt(100); @@ -148,6 +167,7 @@ export default function PoolModal({ setInputValue( (formatTokenAmount({ value: newValue, expo: decimals }) ?? '').toString(), ); + if (mode === 'Deposit') setDepositPct(percentage); if (mode === 'Withdraw') setWithdrawPct(percentage); }, [decimals, balance, mode], @@ -159,6 +179,7 @@ export default function PoolModal({ if (val === '') { setInputError(undefined); setRawValue(BigInt(0)); + if (mode === 'Deposit') setDepositPct(0); if (mode === 'Withdraw') setWithdrawPct(0); return; } @@ -168,15 +189,27 @@ export default function PoolModal({ else { setInputError(undefined); setRawValue(parsed); - if (mode === 'Withdraw' && balance > BigInt(0)) { + if (balance > BigInt(0)) { const pct = Number((parsed * BigInt(100)) / balance); - setWithdrawPct(Math.min(100, Math.max(0, pct))); + const clamped = Math.min(100, Math.max(0, pct)); + if (mode === 'Deposit') setDepositPct(clamped); + if (mode === 'Withdraw') setWithdrawPct(clamped); } } }, [decimals, balance, mode], ); + useEffect(() => { + if (mode === 'Deposit' && balance > BigInt(0)) { + const newValue = (BigInt(depositPct) * balance) / BigInt(100); + setRawValue(newValue); + setInputValue( + (formatTokenAmount({ value: newValue, expo: decimals }) ?? '').toString(), + ); + } + }, [depositPct, mode, balance, decimals]); + useEffect(() => { if (mode === 'Withdraw' && balance > BigInt(0)) { const newValue = (BigInt(withdrawPct) * balance) / BigInt(100); @@ -189,14 +222,125 @@ export default function PoolModal({ const handleClose = useCallback(() => modalContext.closeModal(), [modalContext]); - const isHfAmm = pool.poolType === 'hfAMM'; const poolLabel = pool.name || (isHfAmm ? `${pool.symbol}` : `${pool.symbol} / USDC`); const withdrawReceiveAmount = rawValue; const withdrawReceiveFormatted = formatTokenAmount({ value: withdrawReceiveAmount, expo: decimals, }); - const totalValueUsd = '—'; + // Withdraw: (lp_token / lp_total_supply) * total_liabilities = asset amount out (use totalLiabilities) + const withdrawAssetOut = useMemo(() => { + if (!poolBalance || poolBalance.totalLiabilities === BigInt(0)) + return BigInt(0); + const lpTotalSupply = poolBalance.totalLiabilities; + return (rawValue * lpTotalSupply) / lpTotalSupply; + }, [poolBalance, rawValue]); + const assetDecimals = isHfAmm ? (underlyingToken?.decimals ?? quoteToken?.decimals ?? 6) : decimals; + const withdrawAssetOutFormatted = + formatTokenAmount({ value: withdrawAssetOut, expo: assetDecimals }) ?? '0'; + const totalValueUsd = useMemo(() => { + if (!isHfAmm) return null; + const oracleId = underlyingToken?.oracleId ?? quoteToken?.oracleId ?? pool.oracleId; + const price = oracleId ? oraclePrices[oracleId]?.value : undefined; + if (price == null || price === 0) return null; + const amount = + mode === 'Deposit' ? rawValue : withdrawAssetOut; + const expo = underlyingToken?.decimals ?? quoteToken?.decimals ?? 6; + const value = Number(amount) / 10 ** expo; + const usd = value * price; + return usd; + }, [isHfAmm, mode, underlyingToken, quoteToken, pool.oracleId, oraclePrices, rawValue, withdrawAssetOut]); + + // Deposit: LP amount uses total_liabilities (not reserve) + const expectedLp = useMemo(() => { + if (!poolBalance || poolBalance.totalLiabilities === BigInt(0) || rawValue === BigInt(0)) + return BigInt(0); + return (rawValue * poolBalance.totalLiabilities) / poolBalance.totalLiabilities; + }, [poolBalance, rawValue]); + + const expectedLpFormatted = formatTokenAmount({ + value: expectedLp, + expo: decimals, + }); + + // Pool share (deposit): use total_liabilities, share = expectedLp / (totalLiabilities + expectedLp) + const poolSharePct = useMemo(() => { + if (!poolBalance || rawValue === BigInt(0)) return null; + const tl = poolBalance.totalLiabilities; + const newTotalLp = tl + expectedLp; + if (newTotalLp === BigInt(0)) return null; + const pct = (Number(expectedLp) / Number(newTotalLp)) * 100; + return pct; + }, [poolBalance, rawValue, expectedLp]); + const poolShareDisplay = + poolSharePct != null + ? poolSharePct < 0.01 + ? `${poolSharePct.toFixed(6)}%` + : `${poolSharePct.toFixed(2)}%` + : '—'; + + /** After withdraw: your new share = (your LP - withdrawn) / (total LP supply - withdrawn). */ + const withdrawPoolSharePct = useMemo(() => { + if (!poolBalance || rawValue === BigInt(0)) return null; + const totalSupply = poolBalance.totalLiabilities; + const totalAfter = totalSupply - rawValue; + if (totalAfter <= BigInt(0)) return null; + const userAfter = lpBalance >= rawValue ? lpBalance - rawValue : BigInt(0); + const pct = (Number(userAfter) / Number(totalAfter)) * 100; + const clamped = Math.min(100, Math.max(0, pct)); + return clamped; + }, [poolBalance, rawValue, lpBalance]); + const withdrawPoolShareDisplay = + withdrawPoolSharePct != null + ? withdrawPoolSharePct < 0.01 + ? `${withdrawPoolSharePct.toFixed(6)}%` + : `${withdrawPoolSharePct.toFixed(2)}%` + : '—'; + + const minAmountOutDeposit = useMemo(() => { + if (expectedLp === BigInt(0)) return BigInt(1); + const slippageMultiplier = BigInt(Math.round((100 - slippage) * 1e6)); + const min = (expectedLp * slippageMultiplier) / BigInt(1e8); + return min > BigInt(0) ? min : BigInt(1); + }, [expectedLp, slippage]); + + const minAmountOutWithdraw = useMemo(() => { + if (!poolBalance || poolBalance.totalLiabilities === BigInt(0) || rawValue === BigInt(0)) + return BigInt(1); + const estimatedAssetOut = + (rawValue * poolBalance.totalLiabilities) / poolBalance.totalLiabilities; + if (estimatedAssetOut === BigInt(0)) return BigInt(1); + const slippageMultiplier = BigInt(Math.round((100 - slippage) * 1e6)); + const min = (estimatedAssetOut * slippageMultiplier) / BigInt(1e8); + return min > BigInt(0) ? min : BigInt(1); + }, [poolBalance, rawValue, slippage]); + + const minLpFormatted = formatTokenAmount({ + value: minAmountOutDeposit, + expo: decimals, + }) ?? '0'; + const minWithdrawAssetFormatted = + formatTokenAmount({ value: minAmountOutWithdraw, expo: assetDecimals }) ?? '0'; + + const writeDeposit = useCallback(async () => { + if (depositWithdrawToken == null) return; + await deposit({ + amount: rawValue, + minAmountOut: minAmountOutDeposit, + token: depositWithdrawToken, + noteType: NoteType.Public, + }); + }, [rawValue, minAmountOutDeposit, deposit, depositWithdrawToken]); + + const writeWithdraw = useCallback(async () => { + if (token == null) return; + await withdraw({ + amount: rawValue, + minAmountOut: minAmountOutWithdraw, + token, + noteType: NoteType.Public, + }); + }, [rawValue, minAmountOutWithdraw, withdraw, token]); return (
@@ -265,10 +409,13 @@ export default function PoolModal({ {mode === 'Deposit' && ( <> -
+

- Deposit amounts + {isHfAmm ? 'Deposit amount' : 'Deposit amounts'}

+ +
+

Amount

@@ -282,13 +429,16 @@ export default function PoolModal({ Balance:{' '} {formatTokenAmount({ - value: balanceToken, - expo: pool.decimals, + value: balance, + expo: decimals, })}{' '} - {pool.symbol} + {isHfAmm ? (underlyingToken?.symbol ?? underlyingSymbol) : pool.symbol} - +
@@ -296,7 +446,7 @@ export default function PoolModal({ {PERCENTAGES.map((n) => (
-
- +
+ Deposit percentage + {depositPct}%
-
-

Amount

-
- 0.00 -
- - Balance:{' '} - {formatTokenAmount({ - value: balanceQuote, - expo: quoteToken?.decimals ?? 6, - })}{' '} - USDC - - -
+
+
+
+
+
+

+ You receive (min) +

+
+
+ + + {isHfAmm ? (pool.symbol.startsWith('z') ? pool.symbol : `z${pool.symbol}`) : pool.symbol} +
+ {minLpFormatted ?? '0.00'}
+ {isHfAmm && ( +
+ Total Value + {formatUsd(totalValueUsd)} +
+ )}
{inputError && (

{inputError}

@@ -334,11 +497,7 @@ export default function PoolModal({
Pool Share - ~0.01% -
-
- Est. APR - 24.5% + {poolShareDisplay}
+ ))} +
+
+
+ Withdraw percentage + {withdrawPct}%
-
- {PERCENTAGES.map((n) => ( - - ))} -

- You'll receive + You receive (min)

-
-
- - {pool.symbol} + {isHfAmm + ? ( +
+
+ + {underlyingToken?.symbol ?? underlyingSymbol} +
+ {minWithdrawAssetFormatted} +
+ ) + : ( + <> +
+
+ + {pool.symbol} +
+ {minWithdrawAssetFormatted} +
+
+
+ + USDC +
+ +
+ + )} + {isHfAmm && ( +
+ Total Value + {formatUsd(totalValueUsd)}
- {withdrawReceiveFormatted ?? '0'} -
-
-
- - USDC -
- -
-
- Total Value - ${totalValueUsd} -
+ )}
-
- -
-

- Impermanent Loss Notice -

-

- Withdrawing now realizes any impermanent loss. Your position may - have experienced IL since deposit. If you deposited at a - different price ratio, you may receive fewer tokens than - expected. -

+
+ +
+ Remaining pool share + {withdrawPoolShareDisplay}
-
-
- Unclaimed Fees - +$0.00 -
-
- Network Fee - + {!isHfAmm && ( +
+ +
+

+ Impermanent Loss Notice +

+

+ Withdrawing now realizes any impermanent loss. Your position may + have experienced IL since deposit. If you deposited at a + different price ratio, you may receive fewer tokens than + expected. +

+
-
+ )}
)} {pool.name} + {isHfAmm && ( + + hfAMM + + )} {feeTier}
@@ -69,6 +85,12 @@ export function PositionCard({ Liquidity ${tvlFormatted}
+ {isHfAmm && positionUsd != null && ( +
+ Value + {formatUsd(positionUsd)} +
+ )}
Fees earned $0.00 @@ -84,10 +106,18 @@ export function PositionCard({ )} {isSlim && ( -
- Your deposit - {liquidityFormatted} -
+ <> +
+ Your deposit + {liquidityFormatted} +
+ {isHfAmm && positionUsd != null && ( +
+ Value + {formatUsd(positionUsd)} +
+ )} + )}
diff --git a/src/components/Slippage.tsx b/src/components/Slippage.tsx index d74739a..9338280 100644 --- a/src/components/Slippage.tsx +++ b/src/components/Slippage.tsx @@ -122,6 +122,14 @@ const Slippage = ({ slippage, onSlippageChange }: SlippageProps) => {
)}
+ + diff --git a/src/hooks/useDeposit.tsx b/src/hooks/useDeposit.tsx index f303bd3..37f046b 100644 --- a/src/hooks/useDeposit.tsx +++ b/src/hooks/useDeposit.tsx @@ -4,6 +4,7 @@ import { API } from '@/lib/config'; import { compileDepositTransaction } from '@/lib/ZoroDepositNote'; import { ZoroContext } from '@/providers/ZoroContext'; import { type TokenConfig } from '@/providers/ZoroProvider'; +import { TransactionType } from '@demox-labs/miden-wallet-adapter'; import { NoteType } from '@miden-sdk/miden-sdk'; import { useCallback, useContext, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; @@ -44,9 +45,12 @@ export const useDeposit = () => { userAccountId: accountId, client, noteType, - }), + }) ); - const txId = await requestTransaction({ type: 'Custom', payload: tx }); + const txId = await requestTransaction({ + type: TransactionType.Custom, + payload: tx, + }); await syncState(); if (noteType === NoteType.Private) { const serialized = btoa( diff --git a/src/hooks/useLaunchpad.ts b/src/hooks/useLaunchpad.ts new file mode 100644 index 0000000..719cd35 --- /dev/null +++ b/src/hooks/useLaunchpad.ts @@ -0,0 +1,40 @@ +import { ZoroContext } from '@/providers/ZoroContext'; +import { useCallback, useContext, useMemo, useState } from 'react'; + +interface FaucetParams { + symbol: string; + decimals: number; + initialSupply: bigint; +} + +const useLaunchpad = () => { + const [error, setError] = useState(''); + const { accountId, createFaucet, mintFromFaucet } = useContext(ZoroContext); + const launchToken = useCallback(async (params: FaucetParams) => { + try { + if (!accountId) { + throw new Error('User must be logged in to use the launchpad'); + } + const faucet = await createFaucet(params); + if (faucet && accountId) { + const txId = await mintFromFaucet(faucet.id(), accountId, params.initialSupply); + return txId; + } else throw new Error('Faucet failed creating'); + } catch (e) { + console.log(e); + if (typeof e?.toString === 'function') { + setError(e.toString()); + } else { + setError('Error on launching token, check console for more details.'); + } + } + }, [createFaucet, setError, mintFromFaucet, accountId]); + + const value = useMemo(() => ({ + launchToken, + error, + }), [launchToken, error]); + return value; +}; + +export default useLaunchpad; diff --git a/src/hooks/useNewXykPool.ts b/src/hooks/useNewXykPool.ts new file mode 100644 index 0000000..565f57f --- /dev/null +++ b/src/hooks/useNewXykPool.ts @@ -0,0 +1,4 @@ +const useSpawnNewPool = () => { +}; + +export default useSpawnNewPool; diff --git a/src/hooks/useTokensWithBalance.ts b/src/hooks/useTokensWithBalance.ts new file mode 100644 index 0000000..465e770 --- /dev/null +++ b/src/hooks/useTokensWithBalance.ts @@ -0,0 +1,49 @@ +import { ZoroContext } from '@/providers/ZoroContext'; +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { useCallback, useContext, useEffect, useState } from 'react'; + +/** + * Returns tokens from context that the user has a balance for (balance > 0). + * Used to drive token selects that only show assets the user holds. + */ +export function useTokensWithBalance(): { + tokensWithBalance: TokenConfig[]; + loading: boolean; +} { + const { accountId, getBalance, tokens } = useContext(ZoroContext); + const [tokensWithBalance, setTokensWithBalance] = useState([]); + const [loading, setLoading] = useState(true); + + const refresh = useCallback(async () => { + const list = Object.values(tokens); + if (!accountId || list.length === 0) { + setTokensWithBalance([]); + setLoading(false); + return; + } + setLoading(true); + try { + const withBalances = await Promise.all( + list.map(async (t) => { + const balance = await getBalance(accountId, t.faucetId); + return { token: t, balance }; + }), + ); + const filtered = withBalances + .filter(({ balance }) => balance > 0n) + .map(({ token }) => token); + setTokensWithBalance(filtered); + } catch (e) { + console.error('useTokensWithBalance:', e); + setTokensWithBalance([]); + } finally { + setLoading(false); + } + }, [accountId, getBalance, tokens]); + + useEffect(() => { + refresh(); + }, [refresh]); + + return { tokensWithBalance, loading }; +} diff --git a/src/hooks/useWithdraw.tsx b/src/hooks/useWithdraw.tsx index d88ec1b..27b3f63 100644 --- a/src/hooks/useWithdraw.tsx +++ b/src/hooks/useWithdraw.tsx @@ -4,6 +4,7 @@ import { API } from '@/lib/config'; import { compileWithdrawTransaction } from '@/lib/ZoroWithdrawNote'; import { ZoroContext } from '@/providers/ZoroContext'; import { type TokenConfig } from '@/providers/ZoroProvider'; +import { TransactionType } from '@demox-labs/miden-wallet-adapter'; import { NoteType } from '@miden-sdk/miden-sdk'; import { useCallback, useContext, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; @@ -44,9 +45,12 @@ export const useWithdraw = () => { userAccountId: accountId, client, noteType, - }), + }) ); - const txId = await requestTransaction({ type: 'Custom', payload: tx }); + const txId = await requestTransaction({ + type: TransactionType.Custom, + payload: tx, + }); await syncState(); if (noteType === NoteType.Private) { diff --git a/src/pages/Launchpad.tsx b/src/pages/Launchpad.tsx new file mode 100644 index 0000000..074acac --- /dev/null +++ b/src/pages/Launchpad.tsx @@ -0,0 +1,197 @@ +import { Footer } from '@/components/Footer'; +import { Header } from '@/components/Header'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import { UnifiedWalletButton } from '@/components/UnifiedWalletButton'; +import { useClaimNotes } from '@/hooks/useClaimNotes'; +import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; + +import { ZoroContext } from '@/providers/ZoroContext'; +import { type FaucetMintResult, mintFromFaucet } from '@/services/faucet'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; + +interface MintStatus { + readonly isLoading: boolean; + readonly lastResult: FaucetMintResult | null; + readonly lastAttempt: number; + readonly showMessage: boolean; +} + +type TokenMintStatuses = Record; + +function Faucet() { + const { connected, address } = useUnifiedWallet(); + const { refreshPendingNotes } = useClaimNotes(); + const [mintStatuses, setMintStatuses] = useState( + {} as TokenMintStatuses, + ); + const { tokens, tokensLoading, startExpectingNotes } = useContext(ZoroContext); + const updateMintStatus = useCallback(( + tokenSymbol: string, + updates: Partial, + ): void => { + setMintStatuses(prev => ({ + ...prev, + [tokenSymbol]: { + ...prev[tokenSymbol], + ...updates, + }, + })); + }, []); + + useEffect(() => { + for (const token of Object.values(tokens)) { + // init token states + if (!mintStatuses[token.faucetIdBech32]) { + // eslint-disable-next-line + updateMintStatus(token.faucetIdBech32, { + isLoading: false, + lastAttempt: 0, + lastResult: null, + showMessage: false, + }); + } + } + }, [tokens, mintStatuses, setMintStatuses, updateMintStatus]); + + const requestTokens = useCallback(async (tokenFaucetId: string): Promise => { + if (!connected || !address) { + return; + } + const token = tokens[tokenFaucetId]; + if (!token || !token.faucetIdBech32) { + return; + } + updateMintStatus(tokenFaucetId, { + isLoading: true, + lastAttempt: Date.now(), + showMessage: false, + }); + startExpectingNotes(); + + try { + const result = await mintFromFaucet( + address.split('_')[0], + token.faucetIdBech32, + ); + updateMintStatus(tokenFaucetId, { + isLoading: false, + lastResult: result, + showMessage: false, + }); + // Refresh pending notes count after successful mint (sync with network) + if (result.success) { + setTimeout(() => refreshPendingNotes(), 2000); + } + setTimeout(() => { + updateMintStatus(tokenFaucetId, { + showMessage: true, + }); + }, 100); + setTimeout(() => { + updateMintStatus(tokenFaucetId, { + showMessage: false, + }); + }, 5100); + } catch (error) { + const errorResult: FaucetMintResult = { + success: false, + message: error instanceof Error ? error.message : 'Unknown error', + }; + updateMintStatus(tokenFaucetId, { + isLoading: false, + lastResult: errorResult, + showMessage: false, + }); + setTimeout(() => { + updateMintStatus(tokenFaucetId, { + showMessage: true, + }); + }, 100); + setTimeout(() => { + updateMintStatus(tokenFaucetId, { + showMessage: false, + }); + }, 5100); + } + }, [ + connected, + address, + updateMintStatus, + tokens, + refreshPendingNotes, + startExpectingNotes, + ]); + + const getButtonText = (tokenSymbol: string, status: MintStatus): string => { + return status.isLoading ? `Minting ${tokenSymbol}...` : `Request ${tokenSymbol}`; + }; + + const isButtonDisabled = (status: MintStatus): boolean => { + return status.isLoading || !connected; + }; + + if (tokensLoading) { + return ( +
+
+ + +
+
+ ); + } + + if (Object.keys(tokens).length === 0) { + return ( +
+
+
+
+ +
+
+
+ ); + } + + return ( +
+ Launchpad - ZoroSwap | DeFi on Miden + + + + + +
+
+
+
+

Token launchpad

+
+ + + + +
+
+
+ + + +
+
+
+
+
+ ); +} + +export default Faucet; diff --git a/src/pages/LiquidityPools.tsx b/src/pages/LiquidityPools.tsx index 8b120bc..6c9494f 100644 --- a/src/pages/LiquidityPools.tsx +++ b/src/pages/LiquidityPools.tsx @@ -157,8 +157,8 @@ function LiquidityPools() { poolBalance={poolBalance} lpBalance={lpBalance} variant='slim' - onDeposit={() => openPoolModal(pool)} - onWithdraw={() => openPoolModal(pool)} + onDeposit={() => openPoolModal(pool, 'Deposit')} + onWithdraw={() => openPoolModal(pool, 'Withdraw')} /> )) : ( diff --git a/src/pages/PoolDetail.tsx b/src/pages/PoolDetail.tsx index 31699b9..159d576 100644 --- a/src/pages/PoolDetail.tsx +++ b/src/pages/PoolDetail.tsx @@ -9,7 +9,7 @@ import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; import { useOrderUpdates } from '@/hooks/useWebSocket'; import { ModalContext } from '@/providers/ModalContext'; import { ZoroContext } from '@/providers/ZoroContext'; -import { prettyBigintFormat, truncateId } from '@/utils/format'; +import { fullNumberBigintFormat, prettyBigintFormat, truncateId } from '@/utils/format'; import { AlertTriangle, ExternalLink, ArrowLeft } from 'lucide-react'; import { useCallback, @@ -24,11 +24,26 @@ import AssetIcon from '@/components/AssetIcon'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { TradingViewCandlesChart } from '@/components/TradingViewCandlesChart'; +import { cn } from '@/lib/utils'; import { getMockPoolCandles, getMockRecentTransactions } from '@/mocks/poolDetailMocks'; const feeTierForSymbol = (symbol: string) => /USDC|USDT|DAI|BUSD/i.test(symbol) ? '0.01%' : '0.30%'; +/** Saturation = (reserve / total_liabilities) as percentage (can exceed 100). hfAMM only. */ +function getSaturationPercent(poolBalance: { reserve: bigint; totalLiabilities: bigint }): number | null { + const { reserve, totalLiabilities } = poolBalance; + if (totalLiabilities === BigInt(0)) return null; + return (Number(reserve) / Number(totalLiabilities)) * 100; +} + +function getSaturationColorClass(pct: number): string { + if (pct < 15 || pct > 185) return 'text-red-600 border-red-600/30 bg-red-500/10'; + if ((pct >= 15 && pct < 30) || (pct >= 170 && pct <= 185)) return 'text-yellow-600 border-yellow-600/30 bg-yellow-500/10'; + if (pct >= 30 && pct < 170) return 'text-green-600 border-green-600/30 bg-green-500/10'; + return 'text-muted-foreground border-border bg-muted/30'; +} + const TYPE_COLORS: Record = { Swap: 'text-primary', Add: 'text-green-600', @@ -103,6 +118,22 @@ export default function PoolDetail() { [modalContext, refetchPoolsInfo, openOrderStatusModal, lpBalances], ); + const mockCandles = useMemo(() => { + if (!pool) return []; + return getMockPoolCandles({ + seedKey: pool.faucetIdBech32, + range: chartRange, + }); + }, [pool?.faucetIdBech32, chartRange]); + + const mockRecentTxs = useMemo(() => { + if (!pool) return []; + return getMockRecentTransactions({ + seedKey: pool.faucetIdBech32, + baseSymbol: pool.symbol, + }); + }, [pool?.faucetIdBech32, pool?.symbol]); + if (!pool || !poolBalance) { return (
@@ -120,25 +151,14 @@ export default function PoolDetail() { const decimals = pool.decimals; const feeTier = feeTierForSymbol(pool.symbol); - const tvlFormatted = prettyBigintFormat({ + const tvlFormatted = fullNumberBigintFormat({ value: poolBalance.totalLiabilities, expo: decimals, }); const isHfAmm = pool.poolType === 'hfAMM'; + const saturationPercent = isHfAmm ? getSaturationPercent(poolBalance) : null; + const saturationColor = saturationPercent != null ? getSaturationColorClass(saturationPercent) : ''; const pairLabel = isHfAmm ? `${pool.symbol}` : `${pool.symbol} / USDC`; - const mockCandles = useMemo(() => { - return getMockPoolCandles({ - seedKey: pool.faucetIdBech32, - range: chartRange, - }); - }, [pool.faucetIdBech32, chartRange]); - - const mockRecentTxs = useMemo(() => { - return getMockRecentTransactions({ - seedKey: pool.faucetIdBech32, - baseSymbol: pool.symbol, - }); - }, [pool.faucetIdBech32, pool.symbol]); return (
@@ -217,12 +237,32 @@ export default function PoolDetail() {

${tvlFormatted}

+ {isHfAmm && saturationPercent != null && ( + + +

+ Saturation +

+

+ + {saturationPercent.toFixed(2)}% + +

+
+
+ )}

APR (est.)

-

24.5%

+

@@ -230,7 +270,7 @@ export default function PoolDetail() {

24H Volume

-

$12,500

+

@@ -238,7 +278,7 @@ export default function PoolDetail() {

24H Fees

-

$37.50

+

@@ -276,8 +316,8 @@ export default function PoolDetail() { {pool.symbol}
-

21.56

-

$2,180.00

+

{prettyBigintFormat({ value: poolBalance.reserve, expo: decimals })}

+

@@ -286,17 +326,32 @@ export default function PoolDetail() { USDC
-

45,020.00

-

$1.00

+

{prettyBigintFormat({ value: poolBalance.totalLiabilities, expo: decimals })}

+

-
-
-
-
-

- ETH 48% · USDC 52% -

+ {(() => { + const total = poolBalance.reserve + poolBalance.totalLiabilities; + const reservePct = total > 0n ? Number((poolBalance.reserve * 100n) / total) : 50; + const liabPct = total > 0n ? Number((poolBalance.totalLiabilities * 100n) / total) : 50; + return ( + <> +
+
+
+
+

+ {total > 0n ? `${pool.symbol} ${reservePct}% · USDC ${liabPct}%` : '—'} +

+ + ); + })()} )} @@ -313,16 +368,28 @@ export default function PoolDetail() {
7D Volume - $110,800 +
24h Transactions - 142 +
Total Liquidity ${tvlFormatted}
+ {isHfAmm && ( + <> +
+ Total Liabilities + {fullNumberBigintFormat({ value: poolBalance.totalLiabilities, expo: decimals })} +
+
+ Reserve + {fullNumberBigintFormat({ value: poolBalance.reserve, expo: decimals })} +
+ + )} @@ -336,9 +403,9 @@ export default function PoolDetail() {

- This pool's tokens have moderate price correlation. Estimated - impermanent loss at ±25% price divergence is -5.7%. Consider - concentrated ranges carefully. + This pool's tokens may have price correlation. Impermanent + loss is possible when prices move. Consider concentrated ranges + carefully.

@@ -346,39 +413,41 @@ export default function PoolDetail() {
- - - Recent Transactions - - -
- - - - - - - - - - - - {mockRecentTxs.map((tx, i) => ( - - - - - - + {!isHfAmm && ( + + + Recent Transactions + + +
+
TypeAmount inAmount outAccountTime
- {tx.type} - {tx.amountIn}{tx.amountOut}{tx.account}{tx.timeAgo}
+ + + + + + + - ))} - -
TypeAmount inAmount outAccountTime
-
-
-
+ + + {mockRecentTxs.map((tx, i) => ( + + + {tx.type} + + {tx.amountIn} + {tx.amountOut} + {tx.account} + {tx.timeAgo} + + ))} + + +
+ + + )} diff --git a/src/providers/UnifiedWalletProvider.tsx b/src/providers/UnifiedWalletProvider.tsx index 1683ea6..c0261ec 100644 --- a/src/providers/UnifiedWalletProvider.tsx +++ b/src/providers/UnifiedWalletProvider.tsx @@ -1,12 +1,12 @@ import { clientMutex } from '@/lib/clientMutex'; import { createNetworkId, NETWORK } from '@/lib/config'; +import { TransactionType, useWallet } from '@demox-labs/miden-wallet-adapter'; +import { useAccount, useLogout } from '@getpara/react-sdk-lite'; import { AccountId, AccountInterface, TransactionRequest as TxRequest, } from '@miden-sdk/miden-sdk'; -import { TransactionType, useWallet } from '@demox-labs/miden-wallet-adapter'; -import { useAccount, useLogout } from '@getpara/react-sdk-lite'; import { useParaMiden } from '@miden-sdk/use-miden-para-react'; import { type ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { ParaClientContext } from './ParaClientContext'; @@ -52,7 +52,7 @@ export function UnifiedWalletProvider({ children }: UnifiedWalletProviderProps) async (tx: TransactionRequest): Promise => { if (walletType === 'miden') { // Delegate to Miden wallet adapter - if ('type' in tx && tx.type === 'Custom') { + if ('type' in tx && tx.type === TransactionType.Custom) { return midenWallet.requestTransaction?.({ type: TransactionType.Custom, payload: tx.payload, @@ -77,7 +77,10 @@ export function UnifiedWalletProvider({ children }: UnifiedWalletProviderProps) const accountId = AccountId.fromHex(paraMidenAccountId); // Submit the transaction - const txHash = await paraMidenClient.submitNewTransaction(accountId, txRequest); + const txHash = await paraMidenClient.submitNewTransaction( + accountId, + txRequest, + ); return txHash.toHex(); } @@ -177,7 +180,9 @@ export function UnifiedWalletProvider({ children }: UnifiedWalletProviderProps) ]); // Para client for internal use by ZoroProvider (handles locking) - const paraClientValue = walletType === 'para' ? paraMidenClient ?? undefined : undefined; + const paraClientValue = walletType === 'para' + ? paraMidenClient ?? undefined + : undefined; return ( diff --git a/src/providers/ZoroContext.ts b/src/providers/ZoroContext.ts index d2b822d..8fdf231 100644 --- a/src/providers/ZoroContext.ts +++ b/src/providers/ZoroContext.ts @@ -1,7 +1,14 @@ import type { PoolInfo } from '@/hooks/usePoolsInfo'; -import type { Account, AccountId, ConsumableNoteRecord, Note, RpcClient, WebClient } from '@miden-sdk/miden-sdk'; +import type { + Account, + AccountId, + ConsumableNoteRecord, + Note, + RpcClient, + WebClient, +} from '@miden-sdk/miden-sdk'; import { createContext } from 'react'; -import type { TokenConfig } from './ZoroProvider'; +import type { FaucetParams, TokenConfig } from './ZoroProvider'; type ZoroProviderState = { poolAccountId?: AccountId; @@ -22,6 +29,13 @@ type ZoroProviderState = { isExpectingNotes: boolean; startExpectingNotes: () => void; refreshPendingNotes: () => Promise; + // XYK pools + createFaucet: (params: FaucetParams) => Promise; + mintFromFaucet: ( + faucetId: AccountId, + accountId: AccountId, + amount: bigint, + ) => Promise; }; const initialState: ZoroProviderState = { @@ -38,6 +52,8 @@ const initialState: ZoroProviderState = { isExpectingNotes: false, startExpectingNotes: () => {}, refreshPendingNotes: () => Promise.resolve(), + createFaucet: () => Promise.resolve(undefined), + mintFromFaucet: () => Promise.resolve(''), }; export const ZoroContext = createContext(initialState); diff --git a/src/providers/ZoroProvider.tsx b/src/providers/ZoroProvider.tsx index daab8ca..dbb33ae 100644 --- a/src/providers/ZoroProvider.tsx +++ b/src/providers/ZoroProvider.tsx @@ -1,15 +1,31 @@ import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; -import { NETWORK } from '@/lib/config'; import { clientMutex } from '@/lib/clientMutex'; +import { NETWORK } from '@/lib/config'; import { bech32ToAccountId, instantiateClient } from '@/lib/utils'; -import { AccountId, Address, Endpoint, Note, RpcClient, WebClient } from '@miden-sdk/miden-sdk'; +import { + AccountId, + AccountStorageMode, + Address, + AuthScheme, + Endpoint, + Note, + NoteType, + RpcClient, + WebClient, +} from '@miden-sdk/miden-sdk'; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParaClient } from './ParaClientContext'; import { ZoroContext } from './ZoroContext'; const SYNC_THROTTLE_MS = 1500; +export interface FaucetParams { + symbol: string; + decimals: number; + initialSupply: bigint; +} + export function ZoroProvider({ children, }: { children: ReactNode }) { @@ -19,7 +35,8 @@ export function ZoroProvider({ const paraClient = useParaClient(); // Use accountId from UnifiedWallet if available (Para), otherwise derive from address (Miden) const accountId = useMemo( - () => unifiedAccountId ?? (address ? Address.fromBech32(address).accountId() : undefined), + () => + unifiedAccountId ?? (address ? Address.fromBech32(address).accountId() : undefined), [address, unifiedAccountId], ); const [midenClient, setMidenClient] = useState(undefined); @@ -160,6 +177,49 @@ export function ZoroProvider({ } }, [walletType, accountId, getConsumableNotes]); + // Creates a new faucet + const createFaucet = useCallback((params: FaucetParams) => { + if (!client) { + throw new Error('Client not initialized'); + } + return withClientLock(async () => { + const { symbol, decimals, initialSupply } = params; + const faucet = await client.newFaucet( + AccountStorageMode.public(), + false, + symbol, + decimals, + initialSupply, + AuthScheme.AuthRpoFalcon512, + ); + return faucet; + }); + }, [client, withClientLock]); + + // Mints from a faucet account + const mintFromFaucet = useCallback( + (faucet_id: AccountId, account_id: AccountId, amount: bigint) => { + if (!client) { + throw new Error('Client not initialized'); + } + return withClientLock(async () => { + const mintTxRequest = client.newMintTransactionRequest( + account_id, + faucet_id, + NoteType.Public, + amount, + ); + const mintTxId = await client.submitNewTransaction( + faucet_id, + mintTxRequest, + ); + await client.syncState(); + return mintTxId.toHex(); + }); + }, + [client, withClientLock], + ); + // Periodic refresh for Para wallet users useEffect(() => { if (walletType !== 'para' || !accountId) { @@ -195,8 +255,27 @@ export function ZoroProvider({ isExpectingNotes, startExpectingNotes, refreshPendingNotes, + createFaucet, + mintFromFaucet, }; - }, [accountId, poolsInfo, isPoolsInfoFetched, syncState, getAccount, getBalance, getConsumableNotes, consumeNotes, client, rpcClient, pendingNotesCount, isExpectingNotes, startExpectingNotes, refreshPendingNotes]); + }, [ + accountId, + poolsInfo, + isPoolsInfoFetched, + syncState, + getAccount, + getBalance, + getConsumableNotes, + consumeNotes, + client, + rpcClient, + pendingNotesCount, + isExpectingNotes, + startExpectingNotes, + refreshPendingNotes, + createFaucet, + mintFromFaucet, + ]); return ( diff --git a/src/utils/format.ts b/src/utils/format.ts index 5ceb6a3..c849d93 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -67,6 +67,18 @@ export const formalNumberFormat = ( })).format(val); }; +/** Format USD for display (e.g. $1,234.56). Use for hfAMM total value. */ +export const formatUsd = (val?: number | null): string => { + if (val == null || Number.isNaN(val)) return '—'; + if (val < 0.01 && val > 0) return '<$0.01'; + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(val); +}; + export const formalBigIntFormat = ({ val, expo, round }: { val?: bigint; expo: number; @@ -77,6 +89,15 @@ export const formalBigIntFormat = ({ val, expo, round }: { return formalNumberFormat(numval, round); }; +/** Format bigint as full number (no K/M/B shortening). Use for TVL when you want exact values. */ +export const fullNumberBigintFormat = ( + { value, expo }: { value?: bigint; expo: number }, +) => { + if (value == null) return ''; + const numval = Number(formatUnits(BigInt(value), expo)); + return formalNumberFormat(numval); +}; + export const base64ToHex = (b64: string) => Array.from( atob( From af8a3e772a456e61b12e691ae96067646c36ae06 Mon Sep 17 00:00:00 2001 From: mico Date: Sat, 7 Mar 2026 20:33:45 +0100 Subject: [PATCH 07/49] Xyk pool wizard 2, refactor from slop --- src/components/xyk-wizard/XykWizard.tsx | 217 ++++++++++++++++++ .../xyk-wizard/steps/XykWizardStep1.tsx | 133 +++++++++++ .../xyk-wizard/steps/XykWizardStep2.tsx | 156 +++++++++++++ .../xyk-wizard/steps/XykWizardStep3.tsx | 60 +++++ .../xyk-wizard/steps/XykWizardStep4.tsx | 39 ++++ 5 files changed, 605 insertions(+) create mode 100644 src/components/xyk-wizard/XykWizard.tsx create mode 100644 src/components/xyk-wizard/steps/XykWizardStep1.tsx create mode 100644 src/components/xyk-wizard/steps/XykWizardStep2.tsx create mode 100644 src/components/xyk-wizard/steps/XykWizardStep3.tsx create mode 100644 src/components/xyk-wizard/steps/XykWizardStep4.tsx diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx new file mode 100644 index 0000000..c9f6ac1 --- /dev/null +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -0,0 +1,217 @@ +import { Button } from '@/components/ui/button'; +import useTokensWithBalance from '@/hooks/useTokensWithBalance'; +import { + type CreatedPoolDraft, + readCreatedPools, + writeCreatedPools, +} from '@/lib/poolUtils'; +import { accountIdToBech32 } from '@/lib/utils'; +import { type TokenConfigWithBalance } from '@/providers/ZoroContext'; +import type { AccountId } from '@miden-sdk/miden-sdk'; +import { ChevronLeft } from 'lucide-react'; +import { useCallback, useMemo, useState } from 'react'; +import XykStep1 from './steps/XykWizardStep1'; +import XykStep2 from './steps/XykWizardStep2'; +import XykStep3 from './steps/XykWizardStep3'; +import XykStep4 from './steps/XykWizardStep4'; + +const wizardSteps = [XykStep1, XykStep2, XykStep3, XykStep4]; + +export const XykPairIcon = ( + { symbolA, symbolB, size = 24 }: { symbolA: string; symbolB: string; size?: number }, +) => { + const letterA = (symbolA || '?')[0].toUpperCase(); + const letterB = (symbolB || '?')[0].toUpperCase(); + return ( + + + {letterA} + + + {letterB} + + + ); +}; + +export interface XykWizardForm { + tokenA?: AccountId; + tokenB?: AccountId; + amountA?: bigint; + amountB?: bigint; + feeBps?: number; +} + +export interface XykStepProps { + tokensWithBalance: TokenConfigWithBalance[]; + loading: boolean; + form: XykWizardForm; + setForm: (newForm: XykWizardForm) => void; +} + +const XykWizard = () => { + const [form, setForm] = useState({ + amountA: BigInt(0), + amountB: BigInt(0), + }); + const [step, setStep] = useState(0); + const tokensWithBalance = useTokensWithBalance(); + + const canContinueWizard = useMemo(() => { + switch (step) { + case 0: + return form.tokenA != null && form.tokenB != null && form.tokenA != form.tokenB + && form.feeBps != null && form.feeBps > 0; + case 1: + return form.amountA != null && form.amountA > BigInt(0) && form.amountB != null + && form.amountB > BigInt(0); + case 2: + return true; + default: + return false; + } + }, [step, form]); + + const canGoBackInWizard = useMemo(() => step > 0 && step < wizardSteps.length - 1, [ + step, + ]); + + const next = useCallback(() => { + if (canContinueWizard) { + setStep(Math.min(Math.max(step + 1, 0), wizardSteps.length - 1)); + } + }, [canContinueWizard, step]); + + const back = useCallback(() => { + if (canGoBackInWizard) { + setStep(Math.min(Math.max(step - 1, 0), wizardSteps.length - 1)); + } + }, [canGoBackInWizard, step]); + + const onCreate = useCallback(async () => { + if ( + form.amountA == null || form.amountB == null || form.tokenA == null + || form.tokenB == null || form.feeBps == null + ) { + return; + } + const metadata = tokensWithBalance.metadata; + const tokenAMeta = metadata[accountIdToBech32(form.tokenA)]; + const tokenBMeta = metadata[accountIdToBech32(form.tokenB)]; + const draft: CreatedPoolDraft = { + id: crypto.randomUUID(), + type: 'xyk', + tokenA: { + symbol: tokenAMeta.symbol, + name: tokenAMeta.name, + faucetIdBech32: tokenAMeta.faucetIdBech32, + }, + tokenB: { + symbol: tokenBMeta.symbol, + name: tokenBMeta.name, + faucetIdBech32: tokenBMeta.faucetIdBech32, + }, + feeBps: form.feeBps, + createdAt: Date.now(), + status: 'draft', + }; + const existing = readCreatedPools(); + writeCreatedPools([draft, ...existing]); + setStep(4); + }, [form, tokensWithBalance]); + + const stepTitle = step === 1 + ? 'Create a new Liquidity Pool' + : step === 2 + ? 'Deposit Tokens' + : 'Confirm your details'; + + const activeStep = useMemo(() => { + const Step = wizardSteps[step]; + return ( + + ); + }, [step, form, tokensWithBalance]); + + return ( +
+
+ {step < 3 && ( + + )} +
+ {step < 3 && ( + + Step {step} of 3 + + )} +

+ {step === 1 + ? ( + <> + Create a new{' '} + + Liquidity Pool + + + ) + : stepTitle} +

+
+
+ + {activeStep} + + {/* Actions: primary first; Back only on steps 2–3 (step 1 has chevron only) */} +
+ {step < 3 + ? ( + + ) + : ( + + )} + +
+
+ ); +}; + +export default XykWizard; diff --git a/src/components/xyk-wizard/steps/XykWizardStep1.tsx b/src/components/xyk-wizard/steps/XykWizardStep1.tsx new file mode 100644 index 0000000..cf6e480 --- /dev/null +++ b/src/components/xyk-wizard/steps/XykWizardStep1.tsx @@ -0,0 +1,133 @@ +import { TokenAutocomplete } from '@/components/TokenAutocomplete'; +import { accountIdToBech32, cn } from '@/lib/utils'; +import { AccountId } from '@miden-sdk/miden-sdk'; +import { ArrowRight } from 'lucide-react'; +import { useCallback, useMemo } from 'react'; +import type { XykStepProps } from '../XykWizard'; + +const FEE_TIERS = [ + { bps: 1, label: '0.01%', hint: 'Best for stable pairs' }, + { bps: 5, label: '0.05%', hint: 'Best for stable pairs' }, + { bps: 30, label: '0.30%', hint: 'Best for most pairs' }, + { bps: 100, label: '1.00%', hint: 'Best for exotic pairs' }, +] as const; + +const XykStep1 = ( + { tokensWithBalance, tokenMetadata, form, setForm, loading }: XykStepProps, +) => { + const availableTokens = useMemo(() => { + return Object.values(tokenMetadata ?? {}); + }, [tokenMetadata]); + + const setToken = useCallback((which: 'a' | 'b', id: AccountId) => { + setForm({ ...form, ...(which === 'a' ? { tokenA: id } : { tokenB: id }) }); + }, [form, setForm]); + const setFeeBps = useCallback((feeBps: number) => { + setForm({ ...form, feeBps }); + }, [form, setForm]); + + return ( +
+
+

Select pair

+

+ Choose the base and quote tokens from assets you hold. +

+ {loading + ?

Loading your tokens…

+ : tokensWithBalance.length === 0 + ? ( +

+ You have no token balance. Get tokens from the faucet first. +

+ ) + : ( +
+
+ + setToken('a', AccountId.fromBech32(val))} + excludeFaucetIdBech32={form.tokenB + ? accountIdToBech32(form.tokenB) + : undefined} + placeholder='Base token' + className='w-full' + /> +
+ + + +
+ + setToken('b', AccountId.fromBech32(val))} + excludeFaucetIdBech32={form.tokenA + ? accountIdToBech32(form.tokenA) + : undefined} + placeholder='Quote token' + className='w-full' + /> +
+
+ )} +
+ +
+

Fee tier

+

+ The amount earned providing liquidity. Choose an amount that suits your risk + tolerance and strategy. +

+
+ {FEE_TIERS.map(({ bps, label, hint }) => ( + + ))} +
+
+ ) +
+ ); +}; + +export default XykStep1; diff --git a/src/components/xyk-wizard/steps/XykWizardStep2.tsx b/src/components/xyk-wizard/steps/XykWizardStep2.tsx new file mode 100644 index 0000000..4978819 --- /dev/null +++ b/src/components/xyk-wizard/steps/XykWizardStep2.tsx @@ -0,0 +1,156 @@ +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { prettyBigintFormat } from '@/lib/format'; +import { accountIdToBech32 } from '@/lib/utils'; +import { useCallback, useMemo } from 'react'; +import { formatUnits, parseUnits } from 'viem'; +import { XykPairIcon, type XykStepProps } from '../XykWizard'; + +const XykStep2 = ( + { tokensWithBalance, tokenMetadata, form, setForm }: XykStepProps, +) => { + const tokenA = useMemo(() => { + return tokenMetadata[form.tokenA ? accountIdToBech32(form.tokenA) : '']; + }, [form.tokenA, tokenMetadata]); + const tokenB = useMemo(() => { + return tokenMetadata[form.tokenB ? accountIdToBech32(form.tokenB) : '']; + }, [form.tokenB, tokenMetadata]); + + const setAmountA = useCallback((amount: string) => { + if (tokenA) { + const metadata = tokenMetadata[tokenA.faucetIdBech32]; + const val = parseUnits(amount, metadata.decimals); + setForm({ ...form, amountA: val }); + } + }, [form, setForm, tokenMetadata, tokenA]); + const setAmountB = useCallback((amount: string) => { + if (tokenB) { + const metadata = tokenMetadata[tokenB.faucetIdBech32]; + const val = parseUnits(amount, metadata.decimals); + setForm({ ...form, amountB: val }); + } + }, [form, setForm, tokenMetadata, tokenB]); + const setMaxA = useCallback(() => { + if (tokenA) { + const token = tokensWithBalance.find(({ config }) => { + return tokenA != null && config.faucetIdBech32 === tokenA.faucetIdBech32; + }); + setForm({ ...form, amountA: token?.amount ?? BigInt(0) }); + } + }, [form, setForm, tokenA, tokensWithBalance]); + + const setMaxB = useCallback(() => { + if (tokenB) { + const token = tokensWithBalance.find(({ config }) => { + return tokenB != null && config.faucetIdBech32 === tokenB.faucetIdBech32; + }); + setForm({ ...form, amountB: token?.amount ?? BigInt(0) }); + } + }, [form, setForm, tokenB, tokensWithBalance]); + + const formattedAmountA = useMemo(() => { + if (tokenA) { + return parseFloat(formatUnits(form.amountA ?? BigInt(0), tokenA.decimals)); + } else { + return 0; + } + }, [form.amountA, tokenA]); + const formattedAmountB = useMemo(() => { + if (tokenB) { + return parseFloat(formatUnits(form.amountB ?? BigInt(0), tokenB.decimals)); + } else { + return 0; + } + }, [form.amountB, tokenB]); + + const formattedBalanceA = useMemo(() => { + const token = tokensWithBalance.find(({ config }) => { + return tokenA != null && config.faucetIdBech32 === tokenA.faucetIdBech32; + }); + return prettyBigintFormat({ + value: token?.amount || BigInt(0), + expo: tokenA.decimals, + }); + }, [tokenA, tokensWithBalance]); + const formattedBalanceB = useMemo(() => { + const token = tokensWithBalance.find(({ config }) => { + return tokenB != null && config.faucetIdBech32 === tokenB.faucetIdBech32; + }); + return prettyBigintFormat({ + value: token?.amount || BigInt(0), + expo: tokenB.decimals, + }); + }, [tokenB, tokensWithBalance]); + + return ( +
+

Select your tokens

+
+
+ setAmountA(e.target.value)} + className='flex-1 min-w-0 text-lg border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 h-auto' + /> +
+ + {(tokenA?.symbol ?? '?')[0].toUpperCase()} + + {tokenA.symbol} +
+
+
+ Balance: {formattedBalanceA ?? '0.00'} {tokenA.symbol} + +
+
+
+ setAmountB(e.target.value)} + className='flex-1 min-w-0 text-lg border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 h-auto' + /> +
+ + {(tokenB.symbol || '?')[0].toUpperCase()} + + {tokenB.symbol} +
+
+
+ Balance: {formattedBalanceB ?? '0.00'} {tokenB.symbol} + +
+
+

+ Pair: + {' '} + {tokenA.symbol} / {tokenB.symbol} +

+
+ ); +}; + +export default XykStep2; diff --git a/src/components/xyk-wizard/steps/XykWizardStep3.tsx b/src/components/xyk-wizard/steps/XykWizardStep3.tsx new file mode 100644 index 0000000..722a8e2 --- /dev/null +++ b/src/components/xyk-wizard/steps/XykWizardStep3.tsx @@ -0,0 +1,60 @@ +import { accountIdToBech32 } from '@/lib/utils'; +import { useMemo } from 'react'; +import { formatUnits } from 'viem'; +import type { XykStepProps } from '../XykWizard'; + +const XykStep3 = ({ tokenMetadata, form }: XykStepProps) => { + const tokenA = useMemo(() => { + return tokenMetadata[form.tokenA ? accountIdToBech32(form.tokenA) : '']; + }, [form.tokenA, tokenMetadata]); + const tokenB = useMemo(() => { + return tokenMetadata[form.tokenB ? accountIdToBech32(form.tokenB) : '']; + }, [form.tokenB, tokenMetadata]); + const formattedAmountA = useMemo(() => { + if (tokenA) { + return parseFloat(formatUnits(form.amountA ?? BigInt(0), tokenA.decimals)); + } else { + return 0; + } + }, [form.amountA, tokenA]); + const formattedAmountB = useMemo(() => { + if (tokenB) { + return parseFloat(formatUnits(form.amountB ?? BigInt(0), tokenB.decimals)); + } else { + return 0; + } + }, [form.amountB, tokenB]); + + return ( +
+
+
+ Pair + + {tokenA.symbol}/{tokenB.symbol} + +
+
+ Fee tier + + {(form.feeBps ? form.feeBps / 100 : 0).toFixed(2)}% + +
+
+ Base token deposit + + {formattedAmountA} + +
+
+ Quote token deposit + + {formattedAmountB} + +
+
+
+ ); +}; + +export default XykStep3; diff --git a/src/components/xyk-wizard/steps/XykWizardStep4.tsx b/src/components/xyk-wizard/steps/XykWizardStep4.tsx new file mode 100644 index 0000000..04339bd --- /dev/null +++ b/src/components/xyk-wizard/steps/XykWizardStep4.tsx @@ -0,0 +1,39 @@ +import { Button } from '@/components/ui/button'; +import { emptyFn } from '@/lib/shared'; +import { Check } from 'lucide-react'; +import { Link } from 'react-router-dom'; + +const XykStep4 = () => { + return ( +
+
+

+ Congratulations! +

+
+
+
+ +
+

+ Pool created successfully! +

+

+ View your Pool at Address: (saved to Your pools) +

+
+
+ + + +
+
+ ); +}; + +export default XykStep4; From b38d828b62fdc29f2753048961b3e5d7faa12c74 Mon Sep 17 00:00:00 2001 From: mico Date: Sun, 8 Mar 2026 03:24:49 +0100 Subject: [PATCH 08/49] Restyled wizard, added compiling Xyk pool and deposit note --- package-lock.json | 70 +- package.json | 4 +- src/App.tsx | 6 +- src/components/CreatePoolWizard.tsx | 563 ---------------- src/components/Header.tsx | 2 +- src/components/LiquidityPoolRow.tsx | 34 +- src/components/LiquidityPoolsTable.tsx | 179 +++--- src/components/OrderStatus.tsx | 478 +++++++------- src/components/PoolDetailView.tsx | 2 +- src/components/PoolModal.tsx | 200 +++--- src/components/PositionCard.tsx | 49 +- src/components/Price.tsx | 2 +- src/components/TokenAutocomplete.tsx | 12 +- src/components/UnifiedWalletButton.tsx | 2 +- src/components/xyk-wizard/TokenInput.tsx | 70 ++ src/components/xyk-wizard/XykWizard.tsx | 277 ++++++-- .../xyk-wizard/steps/XykWizardStep1.tsx | 52 +- .../xyk-wizard/steps/XykWizardStep2.tsx | 294 +++++---- .../xyk-wizard/steps/XykWizardStep3.tsx | 4 +- .../xyk-wizard/steps/XykWizardStep4.tsx | 52 +- src/hooks/useBalance.ts | 2 +- src/hooks/useLaunchpad.ts | 51 +- src/hooks/useTokensWithBalance.ts | 67 +- src/lib/DeployXykPool.ts | 141 ++++ src/lib/XykDepositNote.ts | 100 +++ src/lib/ZoroDepositNote.ts | 4 +- src/lib/ZoroSwapNote.ts | 11 +- src/lib/ZoroWithdrawNote.ts | 4 +- src/{utils => lib}/format.ts | 0 src/lib/poolUtils.ts | 32 + src/{utils => lib}/shared.ts | 0 src/masm/accounts/c_prod_pool.masm | 164 +++++ src/masm/accounts/lp_local.masm | 606 ++++++++++++++++++ src/masm/accounts/math.masm | 177 +++++ src/masm/accounts/storage_utils.masm | 134 ++++ src/{lib => masm/accounts}/zoropool.masm | 0 src/{lib => masm/notes}/DEPOSIT.masm | 0 src/{lib => masm/notes}/WITHDRAW.masm | 0 src/{lib => masm/notes}/ZOROSWAP.masm | 0 src/masm/notes/xyk_deposit.masm | 24 + src/masm/notes/xyk_swap.masm | 42 ++ src/masm/notes/xyk_withdraw.masm | 36 ++ src/pages/Launchpad.tsx | 513 ++++++++++----- src/pages/LiquidityPools.tsx | 46 +- src/pages/NewXykPool.tsx | 16 + src/pages/PoolDetail.tsx | 115 ++-- src/pages/Pools.tsx | 55 +- src/providers/ModalContext.tsx | 2 +- src/providers/OracleContext.ts | 2 +- src/providers/ZoroContext.ts | 4 + src/providers/ZoroProvider.tsx | 45 +- 51 files changed, 3158 insertions(+), 1587 deletions(-) delete mode 100644 src/components/CreatePoolWizard.tsx create mode 100644 src/components/xyk-wizard/TokenInput.tsx create mode 100644 src/lib/DeployXykPool.ts create mode 100644 src/lib/XykDepositNote.ts rename src/{utils => lib}/format.ts (100%) create mode 100644 src/lib/poolUtils.ts rename src/{utils => lib}/shared.ts (100%) create mode 100644 src/masm/accounts/c_prod_pool.masm create mode 100644 src/masm/accounts/lp_local.masm create mode 100644 src/masm/accounts/math.masm create mode 100644 src/masm/accounts/storage_utils.masm rename src/{lib => masm/accounts}/zoropool.masm (100%) rename src/{lib => masm/notes}/DEPOSIT.masm (100%) rename src/{lib => masm/notes}/WITHDRAW.masm (100%) rename src/{lib => masm/notes}/ZOROSWAP.masm (100%) create mode 100644 src/masm/notes/xyk_deposit.masm create mode 100644 src/masm/notes/xyk_swap.masm create mode 100644 src/masm/notes/xyk_withdraw.masm create mode 100644 src/pages/NewXykPool.tsx diff --git a/package-lock.json b/package-lock.json index 370d04f..bbcc0cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@demox-labs/miden-wallet-adapter": "^0.10.0", - "@getpara/react-sdk-lite": "^2.13.0", + "@getpara/react-sdk-lite": "^2.15.0", "@miden-sdk/miden-para": "^0.13.3", "@miden-sdk/miden-sdk": "^0.13.2", "@miden-sdk/react": "^0.13.3", @@ -1830,9 +1830,9 @@ "license": "MIT" }, "node_modules/@getpara/core-components": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@getpara/core-components/-/core-components-2.13.0.tgz", - "integrity": "sha512-zzSt8KyLkEP4PD92DQ9GkxR4uiNZNWad6NR2SkOtnjQYIyX7oaLppqbiXyywBiOUjE6uNjvpDcAc3J36URCIsQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@getpara/core-components/-/core-components-2.15.0.tgz", + "integrity": "sha512-UkxZgPxcMXxIvJfDIxyMxbEfBdHSs1i4hmMLWqNqLRE1qeTJ60DQeeW01mo1pm9ZFjIs2PUWn6fm07+iGdKo5A==", "dependencies": { "@stencil/core": "^4.7.0", "color-blend": "^4.0.0", @@ -1843,14 +1843,14 @@ } }, "node_modules/@getpara/core-sdk": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@getpara/core-sdk/-/core-sdk-2.13.0.tgz", - "integrity": "sha512-rhJR/4z4HojALVD40Lsxr3W0DBKrcK2jiub4meetQ/3Vze84oJQnBCgdPDkBNxDIP1LH1H6PUEpYcmWogS4ipQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@getpara/core-sdk/-/core-sdk-2.15.0.tgz", + "integrity": "sha512-8yI0VYSjN4N0XtFn6U8KAli0NWt8osdSqTtupgz0zHhtyaaKvYxf94iBWFuL2YpmM6zuBNbIxFAXnGSrj8CTFg==", "dependencies": { "@celo/utils": "^8.0.2", "@cosmjs/encoding": "^0.32.4", "@ethereumjs/util": "^9.1.0", - "@getpara/user-management-client": "2.13.0", + "@getpara/user-management-client": "2.15.0", "@noble/hashes": "^1.5.0", "base64url": "^3.0.1", "libphonenumber-js": "^1.11.7", @@ -1860,12 +1860,12 @@ } }, "node_modules/@getpara/react-common": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@getpara/react-common/-/react-common-2.13.0.tgz", - "integrity": "sha512-6BdPKMXm+l+U3KT8+w2Xi8gc4v0pOWsxRAwu5bwRhiYdWV7vm30+DaUoVtnf5SPADnaa/vJd1CeR7AWnCJSbmg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@getpara/react-common/-/react-common-2.15.0.tgz", + "integrity": "sha512-brDyHMXYEyZwiJRoEOI3l7BgmfRTbfj6mI3jcbcmWVGr5AGS1TaiouuMzGPwhjlFTXPSP/UJOrNvt0Tc2eDRMg==", "dependencies": { - "@getpara/react-components": "2.13.0", - "@getpara/web-sdk": "2.13.0", + "@getpara/react-components": "2.15.0", + "@getpara/web-sdk": "2.15.0", "@moonpay/moonpay-react": "^1.10.6", "@ramp-network/ramp-instant-sdk": "^4.0.5", "libphonenumber-js": "^1.11.7", @@ -1878,21 +1878,21 @@ } }, "node_modules/@getpara/react-components": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@getpara/react-components/-/react-components-2.13.0.tgz", - "integrity": "sha512-vm3LbLtSG+aeYqN4OKKqd4u9ShDqCj65vX0q9J2iLd/YUI12DnOZwOejfoDsOzrvV7LMeyqEO5FGnSReP04ezg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@getpara/react-components/-/react-components-2.15.0.tgz", + "integrity": "sha512-PeXUVRYAI9G8WITbao3HSR0fyV5nqK0eNKjqficqc84HjEEan/kJsZUUYwDCn/r2eIlm4MbT5XG7R5sydKz8ug==", "dependencies": { - "@getpara/core-components": "2.13.0" + "@getpara/core-components": "2.15.0" } }, "node_modules/@getpara/react-sdk-lite": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@getpara/react-sdk-lite/-/react-sdk-lite-2.13.0.tgz", - "integrity": "sha512-68nT4bnzga7ZzeKehoo4cO6ydNEDN0C/c2cR5D0JT/w2CBnnYwHrwo5UjR1SWG+EiR/pxdu+ASFfgmfe3d8knw==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@getpara/react-sdk-lite/-/react-sdk-lite-2.15.0.tgz", + "integrity": "sha512-/ug8/fChst4zBdC+7wyqQPd9JgItq0Dj2xlGYv9hNaQzdndGWrzz7VmJ804ZDaWHJ1syG7vv4MaMQw/GAxX2BA==", "dependencies": { - "@getpara/react-common": "2.13.0", - "@getpara/react-components": "2.13.0", - "@getpara/web-sdk": "2.13.0", + "@getpara/react-common": "2.15.0", + "@getpara/react-components": "2.15.0", + "@getpara/web-sdk": "2.15.0", "date-fns": "^3.6.0", "framer-motion": "^11.3.31", "libphonenumber-js": "^1.11.7", @@ -1939,28 +1939,28 @@ } }, "node_modules/@getpara/shared": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@getpara/shared/-/shared-1.10.0.tgz", - "integrity": "sha512-xSBzCrQEbm+p32aoIjZcKL+TrfNRRYvuwGQrzvEMLB+vUxvkPMWz2n1uifRKzIVb/1MtIX4lDv5WiNe8xqFS9A==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@getpara/shared/-/shared-1.11.0.tgz", + "integrity": "sha512-B3JCn/K2tNKf4JoXwN2T/MexuZHidqBUtZrM59s2uIHulQDwnW/Vxw2AJzHg/X5P7xxHhhxGI04FbykldMuinw==" }, "node_modules/@getpara/user-management-client": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@getpara/user-management-client/-/user-management-client-2.13.0.tgz", - "integrity": "sha512-2yGNjC8mgOoM1lfzjrYoK6VjvlzBQrwk0XUHKz0nSGeWvfsgQPuEU2jeZqf0cqj9jg+FbXnhuxdJU8aQaTgx8g==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@getpara/user-management-client/-/user-management-client-2.15.0.tgz", + "integrity": "sha512-MbS/S4jzIm02SuBpC0bSBjzM1NkndOGDOAjxHefcD1v8kG3WMVvDgUTYULhwwZbBZiEeWitXZopxXAXEs/ebAQ==", "dependencies": { - "@getpara/shared": "1.10.0", + "@getpara/shared": "1.11.0", "axios": "^1.8.4", "libphonenumber-js": "^1.11.7" } }, "node_modules/@getpara/web-sdk": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@getpara/web-sdk/-/web-sdk-2.13.0.tgz", - "integrity": "sha512-n5pqA7Lz41npksZFwulV3DSRTdZJr75M3AyvT8eSWYZB9+lOtfgKA1x6+MEHNxcQa/diyexguvmH88KmHnXI0w==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@getpara/web-sdk/-/web-sdk-2.15.0.tgz", + "integrity": "sha512-PRYuMk+laF3n+6wMqoncku6aPCG8uCYzS7959uY1899vEhE4OBHWs4Tm09hk+y+Z72htB8gah3nGHFECByN8Pw==", "peer": true, "dependencies": { - "@getpara/core-sdk": "2.13.0", - "@getpara/user-management-client": "2.13.0", + "@getpara/core-sdk": "2.15.0", + "@getpara/user-management-client": "2.15.0", "base64url": "^3.0.1", "buffer": "6.0.3", "cbor-web": "^9.0.2", diff --git a/package.json b/package.json index 0b08d38..aeebe34 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@demox-labs/miden-wallet-adapter": "^0.10.0", - "@getpara/react-sdk-lite": "^2.13.0", + "@getpara/react-sdk-lite": "^2.15.0", "@miden-sdk/miden-para": "^0.13.3", "@miden-sdk/miden-sdk": "^0.13.2", "@miden-sdk/react": "^0.13.3", @@ -36,7 +36,7 @@ "wagmi": "^3.1.0" }, "overrides": { - "@getpara/web-sdk": "2.13.0" + "@getpara/web-sdk": "2.15.0" }, "devDependencies": { "@eslint/css": "^0.14.1", diff --git a/src/App.tsx b/src/App.tsx index 49e444e..dea8198 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,13 +13,15 @@ import { ThemeProvider } from './providers/ThemeProvider'; import '@demox-labs/miden-wallet-adapter-reactui/styles.css'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Bounce, ToastContainer } from 'react-toastify'; +import Launchpad from './pages/Launchpad'; import LiquidityPools from './pages/LiquidityPools'; import PoolDetail from './pages/PoolDetail'; +import NewXykPool from './pages/NewXykPool'; import Pools from './pages/Pools'; import ModalProvider from './providers/ModalProvider'; -import { ZoroProvider } from './providers/ZoroProvider'; import { ParaProviderWrapper } from './providers/ParaProviderWrapper'; import { UnifiedWalletProvider } from './providers/UnifiedWalletProvider'; +import { ZoroProvider } from './providers/ZoroProvider'; const queryClient = new QueryClient(); @@ -29,9 +31,11 @@ function AppRouter() { } /> } /> + } /> } /> } /> } /> + } /> } /> diff --git a/src/components/CreatePoolWizard.tsx b/src/components/CreatePoolWizard.tsx deleted file mode 100644 index a400652..0000000 --- a/src/components/CreatePoolWizard.tsx +++ /dev/null @@ -1,563 +0,0 @@ -import { type AccountId, AccountId as AccountIdClass } from '@miden-sdk/miden-sdk'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { TokenAutocomplete } from '@/components/TokenAutocomplete'; -import { ModalContext } from '@/providers/ModalContext'; -import type { TokenConfig } from '@/providers/ZoroProvider'; -import { ZoroContext } from '@/providers/ZoroContext'; -import { useBalance } from '@/hooks/useBalance'; -import { useTokensWithBalance } from '@/hooks/useTokensWithBalance'; -import { accountIdToBech32, bech32ToAccountId } from '@/lib/utils'; -import { cn } from '@/lib/utils'; -import { ArrowRight, Check, ChevronLeft, X } from 'lucide-react'; -import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; - -/** Two circles with first letter of each symbol (XYK pool style) */ -function XykPairIcon({ symbolA, symbolB, size = 24 }: { symbolA: string; symbolB: string; size?: number }) { - const letterA = (symbolA || '?')[0].toUpperCase(); - const letterB = (symbolB || '?')[0].toUpperCase(); - return ( - - - {letterA} - - - {letterB} - - - ); -} - -const HEX_REGEX = /^(0x)?[0-9a-fA-F]+$/; - -/** Parse faucet ID from bech32 (e.g. mtst1...) or hex (with or without 0x). Returns AccountId or null. */ -function parseFaucetIdToAccountId(value: string): AccountId | null { - const v = value?.trim(); - if (!v) return null; - try { - if (HEX_REGEX.test(v)) { - const hex = v.startsWith('0x') ? v : `0x${v}`; - return AccountIdClass.fromHex(hex); - } - const id = bech32ToAccountId(v); - return id ?? null; - } catch { - return null; - } -} - -function minimalTokenFromFaucetId( - rawInput: string, - symbol: string, - decimals = 18, -): TokenConfig | null { - const v = rawInput?.trim(); - if (!v) return null; - try { - const faucetId = parseFaucetIdToAccountId(v); - if (!faucetId) return null; - const faucetIdBech32 = HEX_REGEX.test(v) ? accountIdToBech32(faucetId) : v; - return { - symbol, - name: symbol, - decimals, - faucetId, - faucetIdBech32, - oracleId: '', - }; - } catch { - return null; - } -} - -/** Resolve faucet input to token: use known token symbol/decimals if faucet matches, else minimal token. */ -function resolveTokenFromFaucetInput( - rawInput: string, - fallbackSymbol: string, - knownTokens: Record | undefined, -): TokenConfig | null { - const minimal = minimalTokenFromFaucetId(rawInput, fallbackSymbol); - if (!minimal || !knownTokens) return minimal; - const known = knownTokens[minimal.faucetIdBech32]; - if (known) { - return { ...minimal, symbol: known.symbol, name: known.name, decimals: known.decimals }; - } - return minimal; -} - -type Step = 1 | 2 | 3 | 4; - -const FEE_TIERS = [ - { bps: 1, label: '0.01%', hint: 'Best for stable pairs' }, - { bps: 5, label: '0.05%', hint: 'Best for stable pairs' }, - { bps: 30, label: '0.30%', hint: 'Best for most pairs' }, - { bps: 100, label: '1.00%', hint: 'Best for exotic pairs' }, -] as const; - -type CreatedPoolDraft = { - id: string; - type: 'xyk'; - tokenA: Pick; - tokenB: Pick; - feeBps: number; - createdAt: number; - status: 'draft'; -}; - -const STORAGE_KEY = 'zoro-created-pools'; - -export function readCreatedPools(): CreatedPoolDraft[] { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return []; - const parsed = JSON.parse(raw) as CreatedPoolDraft[]; - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } -} - -function writeCreatedPools(pools: CreatedPoolDraft[]) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(pools)); -} - -export function clearCreatedPools(): void { - writeCreatedPools([]); -} - -export function CreatePoolWizard({ onCreated }: { onCreated?: () => void }) { - const { closeModal } = useContext(ModalContext); - const { tokens: knownTokens } = useContext(ZoroContext); - const { tokensWithBalance, loading: tokensWithBalanceLoading } = useTokensWithBalance(); - - const [step, setStep] = useState(1); - const [baseFaucetId, setBaseFaucetId] = useState(''); - const [quoteFaucetId, setQuoteFaucetId] = useState(''); - const [feeBps, setFeeBps] = useState(30); - const [amountA, setAmountA] = useState(''); - const [amountB, setAmountB] = useState(''); - const [step1Attempted, setStep1Attempted] = useState(false); - - const tokenA = useMemo( - () => resolveTokenFromFaucetInput(baseFaucetId, 'Base', knownTokens ?? undefined), - [baseFaucetId, knownTokens], - ); - const tokenB = useMemo( - () => resolveTokenFromFaucetInput(quoteFaucetId, 'Quote', knownTokens ?? undefined), - [quoteFaucetId, knownTokens], - ); - - const { - balance: balanceA, - formatted: formattedBalanceA, - refetch: refetchBalanceA, - } = useBalance({ token: tokenA ?? undefined }); - const { - balance: balanceB, - formatted: formattedBalanceB, - refetch: refetchBalanceB, - } = useBalance({ token: tokenB ?? undefined }); - - useEffect(() => { - if (step === 2) { - refetchBalanceA(); - refetchBalanceB(); - } - }, [step, refetchBalanceA, refetchBalanceB]); - - const sameIds = baseFaucetId.trim() === quoteFaucetId.trim() && baseFaucetId.trim().length > 0; - const canContinueStep1 = Boolean( - baseFaucetId.trim() && quoteFaucetId.trim() && !sameIds, - ); - const step1BlockReason = - !baseFaucetId.trim() - ? 'Select base token.' - : !quoteFaucetId.trim() - ? 'Select quote token.' - : sameIds - ? 'Base and quote must be different.' - : null; - const canContinueStep2 = useMemo(() => { - const a = amountA.trim() && parseFloat(amountA) > 0; - const b = amountB.trim() && parseFloat(amountB) > 0; - return a && b; - }, [amountA, amountB]); - - const next = useCallback(() => { - if (step === 1) { - if (!canContinueStep1) { - setStep1Attempted(true); - return; - } - setStep1Attempted(false); - } - if (step === 2 && !canContinueStep2) return; - setStep((s) => (s < 4 ? (s + 1) as Step : s)); - }, [step, canContinueStep1, canContinueStep2]); - - const back = useCallback(() => { - setStep((s) => (s > 1 ? (s - 1) as Step : 1)); - }, []); - - const setMaxA = useCallback(() => { - if (formattedBalanceA) setAmountA(formattedBalanceA); - }, [formattedBalanceA]); - const setMaxB = useCallback(() => { - if (formattedBalanceB) setAmountB(formattedBalanceB); - }, [formattedBalanceB]); - - const onCreate = useCallback(() => { - if (!tokenA || !tokenB) return; - const draft: CreatedPoolDraft = { - id: crypto.randomUUID(), - type: 'xyk', - tokenA: { symbol: tokenA.symbol, name: tokenA.name, faucetIdBech32: tokenA.faucetIdBech32 }, - tokenB: { symbol: tokenB.symbol, name: tokenB.name, faucetIdBech32: tokenB.faucetIdBech32 }, - feeBps, - createdAt: Date.now(), - status: 'draft', - }; - const existing = readCreatedPools(); - writeCreatedPools([draft, ...existing]); - setStep(4); - onCreated?.(); - }, [feeBps, onCreated, tokenA, tokenB]); - - const initialPricePerA = useMemo(() => { - const a = parseFloat(amountA) || 0; - const b = parseFloat(amountB) || 0; - if (a <= 0) return null; - return b / a; - }, [amountA, amountB]); - - // Step 4: Congratulations - if (step === 4) { - return ( -
-
-

- Congratulations! -

- -
-
-
- -
-

Pool created successfully!

-

- View your Pool at Address: (saved to Your pools) -

-
-
- - -
-
- ); - } - - const stepTitle = - step === 1 - ? 'Create a new Liquidity Pool' - : step === 2 - ? 'Deposit Tokens' - : 'Confirm your details'; - - return ( -
-
- {step < 4 && ( - - )} -
- {step < 4 && ( - - Step {step} of 3 - - )} -

- {step === 1 ? ( - <> - Create a new Liquidity Pool - - ) : ( - stepTitle - )} -

-
- -
- - {/* Step 1: Base & quote token selects (from assets user has) + fee tier */} - {step === 1 && ( -
-
-

Select pair

-

- Choose the base and quote tokens from assets you hold. -

- {tokensWithBalanceLoading ? ( -

Loading your tokens…

- ) : tokensWithBalance.length === 0 ? ( -

- You have no token balance. Get tokens from the faucet first. -

- ) : ( -
-
- - -
- - - -
- - -
-
- )} - {step1Attempted && step1BlockReason && ( -

- {step1BlockReason} -

- )} - {canContinueStep1 && ( -

- You are creating a new XYK Pool. Amounts in the next step will use these tokens. -

- )} -
- -
-

Fee tier

-

- The amount earned providing liquidity. Choose an amount that suits your risk tolerance and strategy. -

-
- {FEE_TIERS.map(({ bps, label, hint }) => ( - - ))} -
-
-
- )} - - {/* Step 2: Deposit tokens */} - {step === 2 && tokenA && tokenB && ( -
-

Select your tokens

-
-
- setAmountA(e.target.value)} - className="flex-1 min-w-0 text-lg border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 h-auto" - /> -
- - {(tokenA.symbol || '?')[0].toUpperCase()} - - {tokenA.symbol} -
-
-
- Balance: {formattedBalanceA ?? '0.00'} {tokenA.symbol} - -
-
-
- setAmountB(e.target.value)} - className="flex-1 min-w-0 text-lg border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 h-auto" - /> -
- - {(tokenB.symbol || '?')[0].toUpperCase()} - - {tokenB.symbol} -
-
-
- Balance: {formattedBalanceB ?? '0.00'} {tokenB.symbol} - -
-
-

- Pair: {tokenA.symbol} / {tokenB.symbol} -

-
- )} - - {/* Step 3: Confirm */} - {step === 3 && tokenA && tokenB && ( -
-
-
- Pair - {tokenA.symbol}/{tokenB.symbol} -
-
- Fee tier - {(feeBps / 100).toFixed(2)}% -
-
- Min. Deposit - - {amountB && parseFloat(amountB) >= 0 ? `${parseFloat(amountB).toFixed(1)} ${tokenB.symbol}` : '—'} - -
-
- Max. Deposit - - {amountA && parseFloat(amountA) >= 0 ? `${parseFloat(amountA).toFixed(2)} ${tokenA.symbol}` : '—'} - -
-
- APY - 1.24% - 3.45% -
-
- APR - 2.07% -
-
-
- )} - - {/* Actions: primary first; Back only on steps 2–3 (step 1 has chevron only) */} -
- {step < 3 ? ( - - ) : ( - - )} - {step > 1 && ( - - )} -
-
- ); -} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 5d7897d..cced462 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -13,7 +13,7 @@ export function Header() { }`; return ( -
+
onRowClick?.(pool) : undefined} - onKeyDown={ - isRowClickable - ? (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onRowClick?.(pool); - } - } - : undefined - } + onKeyDown={isRowClickable + ? (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onRowClick?.(pool); + } + } + : undefined} >
@@ -172,7 +174,9 @@ export default LiquidityPoolRow; function getSaturationColorClass(pct: number) { if (pct < 15 || pct > 185) return 'text-red-600 border-red-600/30 bg-red-500/10'; - if ((pct >= 15 && pct < 30) || (pct >= 170 && pct <= 185)) return 'text-yellow-600 border-yellow-600/30 bg-yellow-500/10'; + if ((pct >= 15 && pct < 30) || (pct >= 170 && pct <= 185)) { + return 'text-yellow-600 border-yellow-600/30 bg-yellow-500/10'; + } if (pct >= 30 && pct < 170) return 'text-green-600 border-green-600/30 bg-green-500/10'; return 'text-muted-foreground border-border bg-muted/30'; } diff --git a/src/components/LiquidityPoolsTable.tsx b/src/components/LiquidityPoolsTable.tsx index f4278c7..b18d2fc 100644 --- a/src/components/LiquidityPoolsTable.tsx +++ b/src/components/LiquidityPoolsTable.tsx @@ -1,13 +1,13 @@ import type { PoolBalance } from '@/hooks/usePoolsBalances'; import { type PoolInfo } from '@/hooks/usePoolsInfo'; import type { TokenConfig } from '@/providers/ZoroProvider'; +import { Clock, Droplets, Flame, Loader2, Search, Star } from 'lucide-react'; import { useMemo, useState } from 'react'; import LiquidityPoolRow from './LiquidityPoolRow'; import { poweredByMiden } from './PoweredByMiden'; +import { Button } from './ui/button'; import { Card } from './ui/card'; import { Input } from './ui/input'; -import { Button } from './ui/button'; -import { Search, Flame, Clock, Star, Droplets, Loader2 } from 'lucide-react'; export interface LiquidityPoolsTableProps { poolsInfo?: { poolAccountId?: string; liquidityPools?: PoolInfo[] } | null; @@ -40,8 +40,8 @@ const LiquidityPoolsTable = ({ const q = search.trim().toLowerCase(); list = list.filter( p => - p.name.toLowerCase().includes(q) || - p.symbol.toLowerCase().includes(q), + p.name.toLowerCase().includes(q) + || p.symbol.toLowerCase().includes(q), ); } if (poolFilter === 'stables') { @@ -70,7 +70,11 @@ const LiquidityPoolsTable = ({
- {isLoading ? ( - -
- -
-
- ) : isEmpty ? ( - -
-
- + {isLoading + ? ( + +
+ +
+
+ ) + : isEmpty + ? ( + +
+
+ +
+

+ {noPoolsAtAll ? 'No liquidity pools yet' : 'No pools match your search'} +

+

+ {noPoolsAtAll + ? 'Be the first to create a pool and earn fees from trades. Create a new XYK pool or add liquidity once pools exist.' + : 'Try a different search term or filter to find pools.'} +

+ {noPoolsAtAll && onCreatePool && ( + + )}
-

- {noPoolsAtAll ? 'No liquidity pools yet' : 'No pools match your search'} -

-

- {noPoolsAtAll - ? 'Be the first to create a pool and earn fees from trades. Create a new XYK pool or add liquidity once pools exist.' - : 'Try a different search term or filter to find pools.'} -

- {noPoolsAtAll && onCreatePool && ( - - )} -
- - ) : ( - -
- - - - - - - - - - - - - - {filteredPools.map(pool => { - const balances = poolBalances?.find(b => b.faucetIdBech32 === pool.faucetIdBech32); - const tokenConfig = tokenConfigs?.find(c => c?.faucetIdBech32 === pool.faucetIdBech32); - return balances - ? ( - - ) - : ( - - + + ) + : ( + +
+
PoolTVL ↑SaturationAPR ↑1D VOL ↑7D VOL ↑
-
+ + + + + + + + + + + + + {filteredPools.map(pool => { + const balances = poolBalances?.find(b => + b.faucetIdBech32 === pool.faucetIdBech32 ); - })} - -
PoolTVL ↑SaturationAPR ↑1D VOL ↑7D VOL ↑ +
-
-
- )} + const tokenConfig = tokenConfigs?.find(c => + c?.faucetIdBech32 === pool.faucetIdBech32 + ); + return balances + ? ( + + ) + : ( + + + + ); + })} + + +
+
+ )}
{poweredByMiden} diff --git a/src/components/OrderStatus.tsx b/src/components/OrderStatus.tsx index 801469c..93bf1e3 100644 --- a/src/components/OrderStatus.tsx +++ b/src/components/OrderStatus.tsx @@ -1,7 +1,7 @@ import { Button } from '@/components/ui/button'; +import { formalBigIntFormat, truncateId } from '@/lib/format'; import type { TokenConfig } from '@/providers/ZoroProvider'; import type { OrderStatus } from '@/services/websocket'; -import { formalBigIntFormat, truncateId } from '@/utils/format'; import { CheckCircle, Clock, ExternalLink, Loader2, X, XCircle } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import AssetIcon from './AssetIcon'; @@ -131,19 +131,17 @@ export function OrderStatus({ if (!swapResult) return null; const isLpSuccess = orderStatus === 'executed' && lpDetails; - const successTitle = - lpDetails?.actionType === 'Withdraw' - ? 'Withdrawal Successful!' - : 'Deposit Successful!'; - const successMessage = - lpDetails?.actionType === 'Withdraw' - ? 'Your liquidity has been removed from the pool.' - : 'Your liquidity has been added to the pool.'; + const successTitle = lpDetails?.actionType === 'Withdraw' + ? 'Withdrawal Successful!' + : 'Deposit Successful!'; + const successMessage = lpDetails?.actionType === 'Withdraw' + ? 'Your liquidity has been removed from the pool.' + : 'Your liquidity has been added to the pool.'; const amountFormatted = lpDetails ? formalBigIntFormat({ - val: lpDetails.amount, - expo: lpDetails.token?.decimals ?? 6, - }) + val: lpDetails.amount, + expo: lpDetails.token?.decimals ?? 6, + }) : ''; return ( @@ -168,247 +166,251 @@ export function OrderStatus({ }`} >
- {isLpSuccess ? ( - <> -
- -
-
-
- + {isLpSuccess + ? ( + <> +
+
-

- {successTitle} -

-

- {successMessage} -

-
-
-
- - - - - {lpDetails.token?.symbol} - +
+
+ +
+

+ {successTitle} +

+

+ {successMessage} +

- {lpDetails.actionType === 'Deposit' ? ( - <> -

- Liquidity Added -

-
- - {lpDetails.token?.symbol} - +{amountFormatted} -
- - ) : ( - <> -
- {lpDetails.token?.symbol} - {amountFormatted} -
-
- USDC - -
-
- Fees Claimed - +$0.00 -
- - )} -
-
- -
- + + + +
+
+
+ + Done + + View on Explorer
-
- - - ) : ( - <> -
- {title} - -
-
-
- - - Order {statusDisplay.text} - -
- {orderStatus === 'executed' && ( -

- Your order has been completed successfully! -

- )} - {orderStatus === 'matching' && ( -

- Finding the best price for your order -

- )} - {orderStatus === 'pending' && ( -

- Your order is waiting to be processed -

- )} - {!orderStatus && ( -

- Waiting for order confirmation -

- )} -
- - {lpDetails && ( -
-
- - {lpDetails.actionType} - -
- {formalBigIntFormat({ - val: lpDetails.amount ?? BigInt(0), - expo: lpDetails.token?.decimals || 6, - })} {lpDetails.token?.symbol} -
-
-
- )} - {swapDetails && ( -
-
-
- {formalBigIntFormat({ - val: swapDetails.sellAmount ?? BigInt(0), - expo: swapDetails.sellToken?.decimals || 6, - })} {swapDetails?.sellToken?.symbol} -
- -
- {formalBigIntFormat({ - val: swapDetails.buyAmount ?? BigInt(0), - expo: swapDetails.buyToken?.decimals || 6, - })} {swapDetails?.buyToken?.symbol} + + ) + : ( + <> +
+ {title} +
-
-
- )} - {orderStatus === 'executed' && !lpDetails && ( -
- Claim your tokens in the wallet. -
- )} -
-
- -
- + + + +
+ {orderStatus === 'executed' && ( + )} - - - - -
- {orderStatus === 'executed' && ( - - )} -
-
- - )} +
+
+ + )}
diff --git a/src/components/PoolDetailView.tsx b/src/components/PoolDetailView.tsx index b588b18..67bc438 100644 --- a/src/components/PoolDetailView.tsx +++ b/src/components/PoolDetailView.tsx @@ -1,6 +1,6 @@ import type { PoolBalance } from '@/hooks/usePoolsBalances'; import type { PoolInfo } from '@/hooks/usePoolsInfo'; -import { prettyBigintFormat } from '@/utils/format'; +import { prettyBigintFormat } from '@/lib/format'; import { X } from 'lucide-react'; import AssetIcon from './AssetIcon'; import { Button } from './ui/button'; diff --git a/src/components/PoolModal.tsx b/src/components/PoolModal.tsx index f1d9a30..fd164b3 100644 --- a/src/components/PoolModal.tsx +++ b/src/components/PoolModal.tsx @@ -1,20 +1,20 @@ import { useDeposit } from '@/hooks/useDeposit'; -import { useWithdraw } from '@/hooks/useWithdraw'; import { usePoolsBalances } from '@/hooks/usePoolsBalances'; +import { useWithdraw } from '@/hooks/useWithdraw'; import { DEFAULT_SLIPPAGE } from '@/lib/config'; +import { formatTokenAmount, formatUsd } from '@/lib/format'; +import { useOraclePrices } from '@/providers/OracleContext'; import { ZoroContext } from '@/providers/ZoroContext'; import type { TokenConfig } from '@/providers/ZoroProvider'; -import { useOraclePrices } from '@/providers/OracleContext'; import { NoteType } from '@miden-sdk/miden-sdk'; -import { ChevronDown, Info, Loader, AlertTriangle, X } from 'lucide-react'; +import { AlertTriangle, Info, Loader, X } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { parseUnits } from 'viem'; import { useBalance } from '../hooks/useBalance'; import { type PoolInfo } from '../hooks/usePoolsInfo'; import { ModalContext } from '../providers/ModalContext'; -import { formatTokenAmount, formatUsd } from '../utils/format'; -import type { LpDetails, TxResult } from './OrderStatus'; import AssetIcon from './AssetIcon'; +import type { LpDetails, TxResult } from './OrderStatus'; import Slippage from './Slippage'; import { Button } from './ui/button'; import { Input } from './ui/input'; @@ -58,14 +58,13 @@ export default function PoolModal({ const { data: poolBalancesData } = usePoolsBalances(); const poolBalance = useMemo( () => - poolBalancesData?.find((b) => b.faucetIdBech32 === pool.faucetIdBech32) ?? - null, + poolBalancesData?.find((b) => b.faucetIdBech32 === pool.faucetIdBech32) + ?? null, [poolBalancesData, pool.faucetIdBech32], ); const token = useMemo( - () => - Object.values(tokens).find((t) => t.faucetIdBech32 === pool.faucetIdBech32), + () => Object.values(tokens).find((t) => t.faucetIdBech32 === pool.faucetIdBech32), [tokens, pool.faucetIdBech32], ); const quoteToken = useMemo( @@ -73,9 +72,13 @@ export default function PoolModal({ [tokens], ); /** For hfAMM: LP symbol is "z" + underlying (e.g. zETH → ETH). Use underlying token for deposit/withdraw. */ - const underlyingSymbol = pool.symbol.startsWith('z') ? pool.symbol.slice(1) : pool.symbol; + const underlyingSymbol = pool.symbol.startsWith('z') + ? pool.symbol.slice(1) + : pool.symbol; const underlyingToken = useMemo( - () => Object.values(tokens).find((t) => t.symbol === underlyingSymbol) ?? quoteToken ?? null, + () => + Object.values(tokens).find((t) => t.symbol === underlyingSymbol) ?? quoteToken + ?? null, [tokens, underlyingSymbol, quoteToken], ); const oracleIds = useMemo( @@ -90,18 +93,18 @@ export default function PoolModal({ token, }); const { balance: balanceQuote } = useBalance({ token: quoteToken ?? undefined }); - const { balance: balanceUnderlying } = useBalance({ token: underlyingToken ?? undefined }); + const { balance: balanceUnderlying } = useBalance({ + token: underlyingToken ?? undefined, + }); const isHfAmm = pool.poolType === 'hfAMM'; - const balance = - mode === 'Withdraw' - ? lpBalance ?? BigInt(0) - : isHfAmm - ? (balanceUnderlying ?? balanceQuote ?? BigInt(0)) - : (balanceToken ?? BigInt(0)); - const decimals = - mode === 'Deposit' && isHfAmm - ? (underlyingToken?.decimals ?? quoteToken?.decimals ?? 6) - : pool.decimals; + const balance = mode === 'Withdraw' + ? lpBalance ?? BigInt(0) + : isHfAmm + ? (balanceUnderlying ?? balanceQuote ?? BigInt(0)) + : (balanceToken ?? BigInt(0)); + const decimals = mode === 'Deposit' && isHfAmm + ? (underlyingToken?.decimals ?? quoteToken?.decimals ?? 6) + : pool.decimals; const depositWithdrawToken = isHfAmm ? (underlyingToken ?? quoteToken ?? token) : token; const clearForm = useCallback(() => { @@ -135,10 +138,9 @@ export default function PoolModal({ amount: rawValue, actionType: mode, }); - const txResult = - mode === 'Deposit' - ? { txId: depositTxId, noteId: depositNoteId } - : { txId: withdrawTxId, noteId: withdrawNoteId }; + const txResult = mode === 'Deposit' + ? { txId: depositTxId, noteId: depositNoteId } + : { txId: withdrawTxId, noteId: withdrawNoteId }; setTxResult(txResult); clearForm(); onSuccess(txResult.noteId as string); @@ -230,12 +232,15 @@ export default function PoolModal({ }); // Withdraw: (lp_token / lp_total_supply) * total_liabilities = asset amount out (use totalLiabilities) const withdrawAssetOut = useMemo(() => { - if (!poolBalance || poolBalance.totalLiabilities === BigInt(0)) + if (!poolBalance || poolBalance.totalLiabilities === BigInt(0)) { return BigInt(0); + } const lpTotalSupply = poolBalance.totalLiabilities; return (rawValue * lpTotalSupply) / lpTotalSupply; }, [poolBalance, rawValue]); - const assetDecimals = isHfAmm ? (underlyingToken?.decimals ?? quoteToken?.decimals ?? 6) : decimals; + const assetDecimals = isHfAmm + ? (underlyingToken?.decimals ?? quoteToken?.decimals ?? 6) + : decimals; const withdrawAssetOutFormatted = formatTokenAmount({ value: withdrawAssetOut, expo: assetDecimals }) ?? '0'; const totalValueUsd = useMemo(() => { @@ -243,18 +248,29 @@ export default function PoolModal({ const oracleId = underlyingToken?.oracleId ?? quoteToken?.oracleId ?? pool.oracleId; const price = oracleId ? oraclePrices[oracleId]?.value : undefined; if (price == null || price === 0) return null; - const amount = - mode === 'Deposit' ? rawValue : withdrawAssetOut; + const amount = mode === 'Deposit' ? rawValue : withdrawAssetOut; const expo = underlyingToken?.decimals ?? quoteToken?.decimals ?? 6; const value = Number(amount) / 10 ** expo; const usd = value * price; return usd; - }, [isHfAmm, mode, underlyingToken, quoteToken, pool.oracleId, oraclePrices, rawValue, withdrawAssetOut]); + }, [ + isHfAmm, + mode, + underlyingToken, + quoteToken, + pool.oracleId, + oraclePrices, + rawValue, + withdrawAssetOut, + ]); // Deposit: LP amount uses total_liabilities (not reserve) const expectedLp = useMemo(() => { - if (!poolBalance || poolBalance.totalLiabilities === BigInt(0) || rawValue === BigInt(0)) + if ( + !poolBalance || poolBalance.totalLiabilities === BigInt(0) || rawValue === BigInt(0) + ) { return BigInt(0); + } return (rawValue * poolBalance.totalLiabilities) / poolBalance.totalLiabilities; }, [poolBalance, rawValue]); @@ -272,12 +288,11 @@ export default function PoolModal({ const pct = (Number(expectedLp) / Number(newTotalLp)) * 100; return pct; }, [poolBalance, rawValue, expectedLp]); - const poolShareDisplay = - poolSharePct != null - ? poolSharePct < 0.01 - ? `${poolSharePct.toFixed(6)}%` - : `${poolSharePct.toFixed(2)}%` - : '—'; + const poolShareDisplay = poolSharePct != null + ? poolSharePct < 0.01 + ? `${poolSharePct.toFixed(6)}%` + : `${poolSharePct.toFixed(2)}%` + : '—'; /** After withdraw: your new share = (your LP - withdrawn) / (total LP supply - withdrawn). */ const withdrawPoolSharePct = useMemo(() => { @@ -290,12 +305,11 @@ export default function PoolModal({ const clamped = Math.min(100, Math.max(0, pct)); return clamped; }, [poolBalance, rawValue, lpBalance]); - const withdrawPoolShareDisplay = - withdrawPoolSharePct != null - ? withdrawPoolSharePct < 0.01 - ? `${withdrawPoolSharePct.toFixed(6)}%` - : `${withdrawPoolSharePct.toFixed(2)}%` - : '—'; + const withdrawPoolShareDisplay = withdrawPoolSharePct != null + ? withdrawPoolSharePct < 0.01 + ? `${withdrawPoolSharePct.toFixed(6)}%` + : `${withdrawPoolSharePct.toFixed(2)}%` + : '—'; const minAmountOutDeposit = useMemo(() => { if (expectedLp === BigInt(0)) return BigInt(1); @@ -305,10 +319,13 @@ export default function PoolModal({ }, [expectedLp, slippage]); const minAmountOutWithdraw = useMemo(() => { - if (!poolBalance || poolBalance.totalLiabilities === BigInt(0) || rawValue === BigInt(0)) + if ( + !poolBalance || poolBalance.totalLiabilities === BigInt(0) || rawValue === BigInt(0) + ) { return BigInt(1); - const estimatedAssetOut = - (rawValue * poolBalance.totalLiabilities) / poolBalance.totalLiabilities; + } + const estimatedAssetOut = (rawValue * poolBalance.totalLiabilities) + / poolBalance.totalLiabilities; if (estimatedAssetOut === BigInt(0)) return BigInt(1); const slippageMultiplier = BigInt(Math.round((100 - slippage) * 1e6)); const min = (estimatedAssetOut * slippageMultiplier) / BigInt(1e8); @@ -427,16 +444,19 @@ export default function PoolModal({ />
- Balance:{' '} - {formatTokenAmount({ + Balance: {formatTokenAmount({ value: balance, expo: decimals, })}{' '} - {isHfAmm ? (underlyingToken?.symbol ?? underlyingSymbol) : pool.symbol} + {isHfAmm + ? (underlyingToken?.symbol ?? underlyingSymbol) + : pool.symbol} @@ -474,11 +494,15 @@ export default function PoolModal({
- {isHfAmm ? (pool.symbol.startsWith('z') ? pool.symbol : `z${pool.symbol}`) : pool.symbol} + {isHfAmm + ? (pool.symbol.startsWith('z') ? pool.symbol : `z${pool.symbol}`) + : pool.symbol}
{minLpFormatted ?? '0.00'} @@ -490,9 +514,7 @@ export default function PoolModal({
)}
- {inputError && ( -

{inputError}

- )} + {inputError &&

{inputError}

}
@@ -506,9 +528,7 @@ export default function PoolModal({ className='w-full rounded-lg h-12 text-base' size='lg' > - {isDepositLoading ? ( - - ) : ( + {isDepositLoading ? : ( 'Deposit' )} @@ -535,12 +555,12 @@ export default function PoolModal({ />
- Balance:{' '} - {formatTokenAmount({ + Balance: {formatTokenAmount({ value: lpBalance, expo: decimals, - })}{' '} - {isHfAmm ? (pool.symbol.startsWith('z') ? pool.symbol : `z${pool.symbol}`) : 'LP'} + })} {isHfAmm + ? (pool.symbol.startsWith('z') ? pool.symbol : `z${pool.symbol}`) + : 'LP'} @@ -578,32 +598,35 @@ export default function PoolModal({

{isHfAmm ? ( +
+
+ + {underlyingToken?.symbol ?? underlyingSymbol} +
+ {minWithdrawAssetFormatted} +
+ ) + : ( + <>
- - {underlyingToken?.symbol ?? underlyingSymbol} + + {pool.symbol}
{minWithdrawAssetFormatted}
- ) - : ( - <> -
-
- - {pool.symbol} -
- {minWithdrawAssetFormatted} -
-
-
- - USDC -
- +
+
+ + USDC
- - )} + +
+ + )} {isHfAmm && (
Total Value @@ -626,10 +649,9 @@ export default function PoolModal({ Impermanent Loss Notice

- Withdrawing now realizes any impermanent loss. Your position may - have experienced IL since deposit. If you deposited at a - different price ratio, you may receive fewer tokens than - expected. + Withdrawing now realizes any impermanent loss. Your position may have + experienced IL since deposit. If you deposited at a different price + ratio, you may receive fewer tokens than expected.

@@ -640,9 +662,7 @@ export default function PoolModal({ className='w-full rounded-lg h-12 text-base' size='lg' > - {isWithdrawLoading ? ( - - ) : ( + {isWithdrawLoading ? : ( 'Confirm Withdraw' )} diff --git a/src/components/PositionCard.tsx b/src/components/PositionCard.tsx index 89d4c19..163433e 100644 --- a/src/components/PositionCard.tsx +++ b/src/components/PositionCard.tsx @@ -1,7 +1,7 @@ import type { PoolBalance } from '@/hooks/usePoolsBalances'; import type { PoolInfo } from '@/hooks/usePoolsInfo'; +import { formatUsd, prettyBigintFormat } from '@/lib/format'; import { cn } from '@/lib/utils'; -import { formatUsd, prettyBigintFormat } from '@/utils/format'; import { useOraclePrices } from '@/providers/OracleContext'; import { useMemo } from 'react'; import AssetIcon from './AssetIcon'; @@ -34,12 +34,23 @@ export function PositionCard({ const oraclePrices = useOraclePrices(pool.oracleId ? [pool.oracleId] : []); const price = pool.oracleId ? oraclePrices[pool.oracleId]?.value : undefined; const positionUsd = useMemo(() => { - if (!isHfAmm || price == null || price === 0 || poolBalance.totalLiabilities === BigInt(0)) + if ( + !isHfAmm || price == null || price === 0 + || poolBalance.totalLiabilities === BigInt(0) + ) { return null; + } const valueInAsset = (lpBalance * poolBalance.reserve) / poolBalance.totalLiabilities; const valueHuman = Number(valueInAsset) / 10 ** decimals; return valueHuman * price; - }, [isHfAmm, price, lpBalance, poolBalance.reserve, poolBalance.totalLiabilities, decimals]); + }, [ + isHfAmm, + price, + lpBalance, + poolBalance.reserve, + poolBalance.totalLiabilities, + decimals, + ]); const liquidityFormatted = prettyBigintFormat({ value: lpBalance, expo: decimals, @@ -51,7 +62,11 @@ export function PositionCard({ const isSlim = variant === 'slim'; return ( - +
{isHfAmm @@ -70,7 +85,9 @@ export function PositionCard({
)} - {pool.name} + + {pool.name} + {isHfAmm && ( hfAMM @@ -82,17 +99,23 @@ export function PositionCard({ {!isSlim && ( <>
- Liquidity + + Liquidity + ${tvlFormatted}
{isHfAmm && positionUsd != null && (
- Value + + Value + {formatUsd(positionUsd)}
)}
- Fees earned + + Fees earned + $0.00
@@ -123,7 +146,10 @@ export function PositionCard({
+ {error && ( + + {error} + + )} +
+
+
+ ); +} diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index c9f6ac1..cdc2260 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -1,5 +1,8 @@ import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { UnifiedWalletButton } from '@/components/UnifiedWalletButton'; import useTokensWithBalance from '@/hooks/useTokensWithBalance'; +import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; import { type CreatedPoolDraft, readCreatedPools, @@ -7,14 +10,108 @@ import { } from '@/lib/poolUtils'; import { accountIdToBech32 } from '@/lib/utils'; import { type TokenConfigWithBalance } from '@/providers/ZoroContext'; -import type { AccountId } from '@miden-sdk/miden-sdk'; +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { AccountId } from '@miden-sdk/miden-sdk'; import { ChevronLeft } from 'lucide-react'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; import XykStep1 from './steps/XykWizardStep1'; import XykStep2 from './steps/XykWizardStep2'; import XykStep3 from './steps/XykWizardStep3'; import XykStep4 from './steps/XykWizardStep4'; +export const XYK_WIZARD_STORAGE_KEY = 'zoro-xyk-wizard'; + +type PersistedForm = { + tokenABech32?: string; + tokenBBech32?: string; + amountA?: string; + amountB?: string; + feeBps?: number; +}; + +function readPersistedWizard(): { + step: number; + form: XykWizardForm; +} { + const defaultForm: XykWizardForm = { + amountA: BigInt(0), + amountB: BigInt(0), + }; + try { + const raw = localStorage.getItem(XYK_WIZARD_STORAGE_KEY); + if (!raw) return { step: 0, form: defaultForm }; + const parsed = JSON.parse(raw) as { step?: number; form?: PersistedForm }; + const step = typeof parsed.step === 'number' && parsed.step >= 0 && parsed.step <= 3 + ? parsed.step + : 0; + const f = parsed.form ?? {}; + const form: XykWizardForm = { + amountA: BigInt(0), + amountB: BigInt(0), + }; + if (typeof f.tokenABech32 === 'string' && f.tokenABech32) { + try { + form.tokenA = AccountId.fromBech32(f.tokenABech32); + } catch { + // ignore invalid + } + } + if (typeof f.tokenBBech32 === 'string' && f.tokenBBech32) { + try { + form.tokenB = AccountId.fromBech32(f.tokenBBech32); + } catch { + // ignore invalid + } + } + if (typeof f.amountA === 'string') { + try { + form.amountA = BigInt(f.amountA); + } catch { + // ignore + } + } + if (typeof f.amountB === 'string') { + try { + form.amountB = BigInt(f.amountB); + } catch { + // ignore + } + } + if (typeof f.feeBps === 'number' && f.feeBps > 0) { + form.feeBps = f.feeBps; + } + return { step, form }; + } catch { + return { step: 0, form: defaultForm }; + } +} + +function writePersistedWizard(step: number, form: XykWizardForm) { + try { + const persisted: PersistedForm = {}; + if (form.tokenA != null) persisted.tokenABech32 = accountIdToBech32(form.tokenA); + if (form.tokenB != null) persisted.tokenBBech32 = accountIdToBech32(form.tokenB); + if (form.amountA != null) persisted.amountA = String(form.amountA); + if (form.amountB != null) persisted.amountB = String(form.amountB); + if (form.feeBps != null) persisted.feeBps = form.feeBps; + localStorage.setItem( + XYK_WIZARD_STORAGE_KEY, + JSON.stringify({ step, form: persisted }), + ); + } catch { + // ignore + } +} + +function clearPersistedWizard() { + try { + localStorage.removeItem(XYK_WIZARD_STORAGE_KEY); + } catch { + // ignore + } +} + const wizardSteps = [XykStep1, XykStep2, XykStep3, XykStep4]; export const XykPairIcon = ( @@ -50,19 +147,23 @@ export interface XykWizardForm { export interface XykStepProps { tokensWithBalance: TokenConfigWithBalance[]; + tokenMetadata: Record; loading: boolean; form: XykWizardForm; setForm: (newForm: XykWizardForm) => void; + restart: () => void; } const XykWizard = () => { - const [form, setForm] = useState({ - amountA: BigInt(0), - amountB: BigInt(0), - }); - const [step, setStep] = useState(0); + const { connected } = useUnifiedWallet(); + const [form, setForm] = useState(() => readPersistedWizard().form); + const [step, setStep] = useState(() => readPersistedWizard().step); const tokensWithBalance = useTokensWithBalance(); + useEffect(() => { + writePersistedWizard(step, form); + }, [step, form]); + const canContinueWizard = useMemo(() => { switch (step) { case 0: @@ -123,14 +224,22 @@ const XykWizard = () => { }; const existing = readCreatedPools(); writeCreatedPools([draft, ...existing]); - setStep(4); - }, [form, tokensWithBalance]); + next(); + }, [form, tokensWithBalance, next]); - const stepTitle = step === 1 + const stepTitle = step === 0 ? 'Create a new Liquidity Pool' + : step === 1 + ? 'Initial liquidity' : step === 2 - ? 'Deposit Tokens' - : 'Confirm your details'; + ? 'Confirm your details' + : 'Congratulations'; + + const restart = useCallback(() => { + clearPersistedWizard(); + setStep(0); + setForm(readPersistedWizard().form); + }, []); const activeStep = useMemo(() => { const Step = wizardSteps[step]; @@ -139,76 +248,120 @@ const XykWizard = () => { form={form} setForm={setForm} tokensWithBalance={tokensWithBalance.tokensWithBalance} + tokenMetadata={tokensWithBalance.metadata} loading={tokensWithBalance.loading} + restart={restart} /> ); - }, [step, form, tokensWithBalance]); + }, [step, form, tokensWithBalance, restart]); - return ( -
-
- {step < 3 && ( + if (!connected) { + return ( + + +

+ Connect your wallet to create a new liquidity pool. +

+ +
+
+ ); + } + + if (tokensWithBalance.loading) { + return ( + + +

Loading your tokens…

+
+
+ ); + } + + if (tokensWithBalance.tokensWithBalance.length === 0) { + return ( + + +

+ Missing tokens to launch a new pool +

+

+ You need tokens to create a liquidity pool. Launch your token on the + launchpad, or get tokens from the faucet, then come back here to create a + pool. +

- )} -
+ + + ); + } + + return ( +
+
+ {step !== wizardSteps.length - 1 + && ( + + )} +
{step < 3 && ( - - Step {step} of 3 + + Step {step + 1} of 3 )} -

- {step === 1 - ? ( - <> - Create a new{' '} - - Liquidity Pool - - - ) - : stepTitle} +

+ {stepTitle}

+ {step !== wizardSteps.length - 1 && ( +
+
+ {step > 0 + ?
+ :
} + {step > 1 + ?
+ :
} +
+ )}
- - {activeStep} - - {/* Actions: primary first; Back only on steps 2–3 (step 1 has chevron only) */} -
- {step < 3 - ? ( +
+ {activeStep} +
+
+ {step !== 2 && step < wizardSteps.length - 1 + && ( - ) - : ( - )} - + {step === 2 && ( + + )}
); diff --git a/src/components/xyk-wizard/steps/XykWizardStep1.tsx b/src/components/xyk-wizard/steps/XykWizardStep1.tsx index cf6e480..ee3c0dc 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep1.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep1.tsx @@ -27,66 +27,69 @@ const XykStep1 = ( }, [form, setForm]); return ( -
-
-

Select pair

-

- Choose the base and quote tokens from assets you hold. +

+ {/* Select pair */} +
+

Select pair

+

+ Choose the tokens you want to provide liquidity for. You can select tokens on + all supported networks.

{loading - ?

Loading your tokens…

+ ?

Loading your tokens…

: tokensWithBalance.length === 0 ? ( -

+

You have no token balance. Get tokens from the faucet first.

) : ( -
+
setToken('a', AccountId.fromBech32(val))} excludeFaucetIdBech32={form.tokenB ? accountIdToBech32(form.tokenB) : undefined} - placeholder='Base token' + placeholder='Select a token' className='w-full' />
- - + +
setToken('b', AccountId.fromBech32(val))} excludeFaucetIdBech32={form.tokenA ? accountIdToBech32(form.tokenA) : undefined} - placeholder='Quote token' + placeholder='Select a token' className='w-full' />
)} -
+
-
-

Fee tier

-

+ {/* Fee tier */} +

+

Fee tier

+

The amount earned providing liquidity. Choose an amount that suits your risk tolerance and strategy.

@@ -97,7 +100,7 @@ const XykStep1 = ( type='button' onClick={() => setFeeBps(bps)} className={cn( - 'rounded-xl border-2 p-4 text-left transition-colors min-h-[88px] flex flex-col justify-center', + 'rounded-xl border-2 px-4 text-left transition-colors md:min-h-[150px] flex flex-col justify-center items-center', form.feeBps === bps ? 'bg-primary text-primary-foreground border-primary shadow-none' : 'bg-card border-border text-foreground hover:border-muted-foreground/50', @@ -105,7 +108,7 @@ const XykStep1 = ( > @@ -113,7 +116,7 @@ const XykStep1 = ( ))}
-
- ) +
); }; diff --git a/src/components/xyk-wizard/steps/XykWizardStep2.tsx b/src/components/xyk-wizard/steps/XykWizardStep2.tsx index 4978819..e7382e2 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep2.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep2.tsx @@ -1,10 +1,11 @@ -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { prettyBigintFormat } from '@/lib/format'; +import { fullNumberBigintFormat } from '@/lib/format'; import { accountIdToBech32 } from '@/lib/utils'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { formatUnits, parseUnits } from 'viem'; -import { XykPairIcon, type XykStepProps } from '../XykWizard'; +import { TokenInput } from '../TokenInput'; +import { type XykStepProps } from '../XykWizard'; + +const PERCENTAGES = [25, 50, 75, 100] as const; const XykStep2 = ( { tokensWithBalance, tokenMetadata, form, setForm }: XykStepProps, @@ -16,139 +17,210 @@ const XykStep2 = ( return tokenMetadata[form.tokenB ? accountIdToBech32(form.tokenB) : '']; }, [form.tokenB, tokenMetadata]); - const setAmountA = useCallback((amount: string) => { + const [amountAStr, setAmountAStr] = useState(''); + const [amountBStr, setAmountBStr] = useState(''); + + useEffect(() => { if (tokenA) { - const metadata = tokenMetadata[tokenA.faucetIdBech32]; - const val = parseUnits(amount, metadata.decimals); - setForm({ ...form, amountA: val }); + setAmountAStr( + form.amountA != null ? formatUnits(form.amountA, tokenA.decimals) : '', + ); } - }, [form, setForm, tokenMetadata, tokenA]); - const setAmountB = useCallback((amount: string) => { + }, [tokenA, form.amountA]); + useEffect(() => { if (tokenB) { - const metadata = tokenMetadata[tokenB.faucetIdBech32]; - const val = parseUnits(amount, metadata.decimals); - setForm({ ...form, amountB: val }); + setAmountBStr( + form.amountB != null ? formatUnits(form.amountB, tokenB.decimals) : '', + ); } - }, [form, setForm, tokenMetadata, tokenB]); + }, [tokenB, form.amountB]); + + const setAmountA = useCallback( + (raw: string) => { + setAmountAStr(raw); + if (raw === '') { + setForm({ ...form, amountA: undefined }); + return; + } + if (tokenA) { + try { + const val = parseUnits(raw, tokenA.decimals); + setForm({ ...form, amountA: val }); + } catch { + setForm({ ...form, amountA: undefined }); + } + } + }, + [form, setForm, tokenA], + ); + const setAmountB = useCallback( + (raw: string) => { + setAmountBStr(raw); + if (raw === '') { + setForm({ ...form, amountB: undefined }); + return; + } + if (tokenB) { + try { + const val = parseUnits(raw, tokenB.decimals); + setForm({ ...form, amountB: val }); + } catch { + setForm({ ...form, amountB: undefined }); + } + } + }, + [form, setForm, tokenB], + ); + const setMaxA = useCallback(() => { if (tokenA) { - const token = tokensWithBalance.find(({ config }) => { - return tokenA != null && config.faucetIdBech32 === tokenA.faucetIdBech32; - }); - setForm({ ...form, amountA: token?.amount ?? BigInt(0) }); + const token = tokensWithBalance.find( + ({ config }) => tokenA != null && config.faucetIdBech32 === tokenA.faucetIdBech32, + ); + const amount = token?.amount ?? BigInt(0); + setForm({ ...form, amountA: amount }); + setAmountAStr(formatUnits(amount, tokenA.decimals)); } }, [form, setForm, tokenA, tokensWithBalance]); const setMaxB = useCallback(() => { if (tokenB) { - const token = tokensWithBalance.find(({ config }) => { - return tokenB != null && config.faucetIdBech32 === tokenB.faucetIdBech32; - }); - setForm({ ...form, amountB: token?.amount ?? BigInt(0) }); + const token = tokensWithBalance.find( + ({ config }) => tokenB != null && config.faucetIdBech32 === tokenB.faucetIdBech32, + ); + const amount = token?.amount ?? BigInt(0); + setForm({ ...form, amountB: amount }); + setAmountBStr(formatUnits(amount, tokenB.decimals)); } }, [form, setForm, tokenB, tokensWithBalance]); - const formattedAmountA = useMemo(() => { - if (tokenA) { - return parseFloat(formatUnits(form.amountA ?? BigInt(0), tokenA.decimals)); - } else { - return 0; - } - }, [form.amountA, tokenA]); - const formattedAmountB = useMemo(() => { - if (tokenB) { - return parseFloat(formatUnits(form.amountB ?? BigInt(0), tokenB.decimals)); - } else { - return 0; - } - }, [form.amountB, tokenB]); + const balanceABigint = useMemo(() => { + const token = tokensWithBalance.find( + ({ config }) => tokenA != null && config.faucetIdBech32 === tokenA.faucetIdBech32, + ); + return token?.amount ?? BigInt(0); + }, [tokenA, tokensWithBalance]); + const balanceBBigint = useMemo(() => { + const token = tokensWithBalance.find( + ({ config }) => tokenB != null && config.faucetIdBech32 === tokenB.faucetIdBech32, + ); + return token?.amount ?? BigInt(0); + }, [tokenB, tokensWithBalance]); + + const setPercentA = useCallback( + (pct: number) => { + if (!tokenA) return; + const amount = (balanceABigint * BigInt(pct)) / BigInt(100); + setForm({ ...form, amountA: amount }); + setAmountAStr(formatUnits(amount, tokenA.decimals)); + }, + [form, setForm, tokenA, balanceABigint], + ); + const setPercentB = useCallback( + (pct: number) => { + if (!tokenB) return; + const amount = (balanceBBigint * BigInt(pct)) / BigInt(100); + setForm({ ...form, amountB: amount }); + setAmountBStr(formatUnits(amount, tokenB.decimals)); + }, + [form, setForm, tokenB, balanceBBigint], + ); const formattedBalanceA = useMemo(() => { - const token = tokensWithBalance.find(({ config }) => { - return tokenA != null && config.faucetIdBech32 === tokenA.faucetIdBech32; - }); - return prettyBigintFormat({ + const token = tokensWithBalance.find( + ({ config }) => tokenA != null && config.faucetIdBech32 === tokenA.faucetIdBech32, + ); + return fullNumberBigintFormat({ value: token?.amount || BigInt(0), - expo: tokenA.decimals, + expo: tokenA?.decimals, }); }, [tokenA, tokensWithBalance]); const formattedBalanceB = useMemo(() => { - const token = tokensWithBalance.find(({ config }) => { - return tokenB != null && config.faucetIdBech32 === tokenB.faucetIdBech32; - }); - return prettyBigintFormat({ + const token = tokensWithBalance.find( + ({ config }) => tokenB != null && config.faucetIdBech32 === tokenB.faucetIdBech32, + ); + return fullNumberBigintFormat({ value: token?.amount || BigInt(0), - expo: tokenB.decimals, + expo: tokenB?.decimals, }); }, [tokenB, tokensWithBalance]); + const balanceAText = `${formattedBalanceA ?? '0.00'} ${tokenA?.symbol ?? ''}`.trim(); + const balanceBText = `${formattedBalanceB ?? '0.00'} ${tokenB?.symbol ?? ''}`.trim(); + + const errorA = + amountAStr !== '' && form.amountA != null && form.amountA > balanceABigint + ? 'Exceeds your balance' + : amountAStr !== '' + && form.amountA != null + && form.amountA < BigInt(0) + ? 'Must be at least 0' + : undefined; + const errorB = + amountBStr !== '' && form.amountB != null && form.amountB > balanceBBigint + ? 'Exceeds your balance' + : amountBStr !== '' + && form.amountB != null + && form.amountB < BigInt(0) + ? 'Must be at least 0' + : undefined; + return ( -
-

Select your tokens

-
-
- setAmountA(e.target.value)} - className='flex-1 min-w-0 text-lg border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 h-auto' - /> -
- - {(tokenA?.symbol ?? '?')[0].toUpperCase()} - - {tokenA.symbol} +
+
+

+ Deposit tokens +

+

+ Specify the token amounts for your liquidity contribution. +

+
+ + {PERCENTAGES.map((pct) => ( + + ))}
-
-
- Balance: {formattedBalanceA ?? '0.00'} {tokenA.symbol} - -
-
-
- setAmountB(e.target.value)} - className='flex-1 min-w-0 text-lg border-0 bg-transparent p-0 shadow-none focus-visible:ring-0 h-auto' - /> -
- - {(tokenB.symbol || '?')[0].toUpperCase()} - - {tokenB.symbol} + } + /> + + {PERCENTAGES.map((pct) => ( + + ))}
-
-
- Balance: {formattedBalanceB ?? '0.00'} {tokenB.symbol} - -
-
-

- Pair: - {' '} - {tokenA.symbol} / {tokenB.symbol} -

+ } + />
); }; diff --git a/src/components/xyk-wizard/steps/XykWizardStep3.tsx b/src/components/xyk-wizard/steps/XykWizardStep3.tsx index 722a8e2..82a1a80 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep3.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep3.tsx @@ -26,8 +26,8 @@ const XykStep3 = ({ tokenMetadata, form }: XykStepProps) => { }, [form.amountB, tokenB]); return ( -
-
+
+
Pair diff --git a/src/components/xyk-wizard/steps/XykWizardStep4.tsx b/src/components/xyk-wizard/steps/XykWizardStep4.tsx index 04339bd..6a74d47 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep4.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep4.tsx @@ -1,36 +1,62 @@ import { Button } from '@/components/ui/button'; import { emptyFn } from '@/lib/shared'; +import { accountIdToBech32 } from '@/lib/utils'; import { Check } from 'lucide-react'; +import { useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; +import { XYK_WIZARD_STORAGE_KEY, type XykStepProps } from '../XykWizard'; -const XykStep4 = () => { +function clearPersistedWizard() { + try { + localStorage.removeItem(XYK_WIZARD_STORAGE_KEY); + } catch { + // ignore + } +} + +const XykStep4 = ({ form, tokenMetadata, restart }: XykStepProps) => { + const tokenA = useMemo(() => { + return tokenMetadata[form.tokenA ? accountIdToBech32(form.tokenA) : '']; + }, [form.tokenA, tokenMetadata]); + const tokenB = useMemo(() => { + return tokenMetadata[form.tokenB ? accountIdToBech32(form.tokenB) : '']; + }, [form.tokenB, tokenMetadata]); + const poolName = useMemo(() => { + return `${tokenA.name}/${tokenB.name}`; + }, [tokenA, tokenB]); + useEffect(() => { + clearPersistedWizard(); + }, []); return ( -
-
-

- Congratulations! -

-
+
-
- +
+
+ +

- Pool created successfully! + Pool {poolName} created successfully!

View your Pool at Address: (saved to Your pools)

-
+
+
); diff --git a/src/hooks/useBalance.ts b/src/hooks/useBalance.ts index 90a9083..09a6d50 100644 --- a/src/hooks/useBalance.ts +++ b/src/hooks/useBalance.ts @@ -1,6 +1,6 @@ +import { formalBigIntFormat, prettyBigintFormat } from '@/lib/format'; import { ZoroContext } from '@/providers/ZoroContext'; import { type TokenConfig } from '@/providers/ZoroProvider'; -import { formalBigIntFormat, prettyBigintFormat } from '@/utils/format'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; interface BalanceParams { diff --git a/src/hooks/useLaunchpad.ts b/src/hooks/useLaunchpad.ts index 719cd35..8852106 100644 --- a/src/hooks/useLaunchpad.ts +++ b/src/hooks/useLaunchpad.ts @@ -1,39 +1,60 @@ +import { accountIdToBech32 } from '@/lib/utils'; import { ZoroContext } from '@/providers/ZoroContext'; import { useCallback, useContext, useMemo, useState } from 'react'; -interface FaucetParams { +export interface FaucetParams { symbol: string; decimals: number; initialSupply: bigint; } +export interface LaunchSuccess { + txId: string; + faucetIdBech32: string; +} + +const MIDENSCAN_BASE = 'https://testnet.midenscan.com'; + +export function getMidenscanTxUrl(txId: string): string { + return `${MIDENSCAN_BASE}/tx/${txId}`; +} + +export function getMidenscanAccountUrl(accountBech32: string): string { + return `${MIDENSCAN_BASE}/account/${accountBech32}`; +} + const useLaunchpad = () => { const [error, setError] = useState(''); const { accountId, createFaucet, mintFromFaucet } = useContext(ZoroContext); - const launchToken = useCallback(async (params: FaucetParams) => { + + const clearError = useCallback(() => setError(''), []); + + const launchToken = useCallback(async (params: FaucetParams): Promise => { + setError(''); try { if (!accountId) { - throw new Error('User must be logged in to use the launchpad'); + throw new Error('Connect your wallet to use the launchpad'); } const faucet = await createFaucet(params); - if (faucet && accountId) { - const txId = await mintFromFaucet(faucet.id(), accountId, params.initialSupply); - return txId; - } else throw new Error('Faucet failed creating'); - } catch (e) { - console.log(e); - if (typeof e?.toString === 'function') { - setError(e.toString()); - } else { - setError('Error on launching token, check console for more details.'); + if (!faucet) { + throw new Error('Faucet creation failed'); } + const faucetIdBech32 = accountIdToBech32(faucet.id()); + const txId = await mintFromFaucet(faucet.id(), accountId, params.initialSupply); + return { txId, faucetIdBech32 }; + } catch (e) { + const message = e instanceof Error ? e.message : typeof e === 'string' ? e : 'Launch failed. Check the console for details.'; + setError(message); + console.error('Launchpad error:', e); + return undefined; } - }, [createFaucet, setError, mintFromFaucet, accountId]); + }, [accountId, createFaucet, mintFromFaucet]); const value = useMemo(() => ({ launchToken, error, - }), [launchToken, error]); + clearError, + }), [launchToken, error, clearError]); return value; }; diff --git a/src/hooks/useTokensWithBalance.ts b/src/hooks/useTokensWithBalance.ts index 465e770..8a2a37f 100644 --- a/src/hooks/useTokensWithBalance.ts +++ b/src/hooks/useTokensWithBalance.ts @@ -1,49 +1,36 @@ -import { ZoroContext } from '@/providers/ZoroContext'; +import { type TokenConfigWithBalance, ZoroContext } from '@/providers/ZoroContext'; import type { TokenConfig } from '@/providers/ZoroProvider'; -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; -/** - * Returns tokens from context that the user has a balance for (balance > 0). - * Used to drive token selects that only show assets the user holds. - */ -export function useTokensWithBalance(): { - tokensWithBalance: TokenConfig[]; - loading: boolean; -} { - const { accountId, getBalance, tokens } = useContext(ZoroContext); - const [tokensWithBalance, setTokensWithBalance] = useState([]); +const useTokensWithBalance = () => { + const { getAvailableTokens, accountId } = useContext(ZoroContext); + const [tokensWithBalance, setTokensWithBalance] = useState< + TokenConfigWithBalance[] + >([]); const [loading, setLoading] = useState(true); const refresh = useCallback(async () => { - const list = Object.values(tokens); - if (!accountId || list.length === 0) { - setTokensWithBalance([]); - setLoading(false); - return; - } setLoading(true); - try { - const withBalances = await Promise.all( - list.map(async (t) => { - const balance = await getBalance(accountId, t.faucetId); - return { token: t, balance }; - }), - ); - const filtered = withBalances - .filter(({ balance }) => balance > 0n) - .map(({ token }) => token); - setTokensWithBalance(filtered); - } catch (e) { - console.error('useTokensWithBalance:', e); - setTokensWithBalance([]); - } finally { - setLoading(false); - } - }, [accountId, getBalance, tokens]); + const tokens = await getAvailableTokens(); + setTokensWithBalance(tokens); + setLoading(false); + }, [getAvailableTokens]); + + const metadata = useMemo(() => { + return tokensWithBalance.reduce((acc, t) => { + return { ...acc, [t.config.faucetIdBech32]: t.config }; + }, {} as Record); + }, [tokensWithBalance]); useEffect(() => { - refresh(); - }, [refresh]); + if (accountId) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setLoading(true); + refresh(); + } + }, [refresh, accountId]); + + return { tokensWithBalance, loading, metadata }; +}; - return { tokensWithBalance, loading }; -} +export default useTokensWithBalance; diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts new file mode 100644 index 0000000..46286c3 --- /dev/null +++ b/src/lib/DeployXykPool.ts @@ -0,0 +1,141 @@ +import { + AccountId, + AccountType, + Felt, + NoteType, + StorageSlot, + WebClient, + Word, +} from '@miden-sdk/miden-sdk'; + +import c_prod from '@/masm/accounts/c_prod_pool.masm?raw'; +import lp_local from '@/masm/accounts/lp_local.masm?raw'; +import math from '@/masm/accounts/math.masm?raw'; +import storage_utils from '@/masm/accounts/storage_utils.masm?raw'; + +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { StorageMap } from '@miden-sdk/miden-sdk'; +import { AccountComponent } from '@miden-sdk/miden-sdk'; +import { AuthSecretKey } from '@miden-sdk/miden-sdk'; +import { AccountBuilder } from '@miden-sdk/miden-sdk'; +import { AccountStorageMode } from '@miden-sdk/miden-sdk'; + +export interface DeployNewPoolParams { + token0: TokenConfig; + token1: TokenConfig; + amount0: bigint; + amount1: bigint; + userAccountId: AccountId; + client: WebClient; + noteType: NoteType; +} + +export interface DeployResult { + txId: string; + noteId: string; + newPool: AccountId; +} + +const build_lp_local_component = (client: WebClient) => { + const builder = client.createCodeBuilder(); + const math_lib = builder.buildLibrary('zoro::math', math); + // const storage_utils_lib = builder.buildLibrary('zoro::storage_utils', storage_utils); + builder.linkStaticLibrary(math_lib); + // builder.linkStaticLibrary(storage_utils_lib) + const c_prod_component = builder.buildLibrary('zoro::c_prod', c_prod); + return c_prod_component; +}; +const build_c_prod_component = (client: WebClient) => { + const builder = client.createCodeBuilder(); + const math_lib = builder.buildLibrary('zoro::math', math); + const storage_utils_lib = builder.buildLibrary('zoro::storage_utils', storage_utils); + builder.linkStaticLibrary(math_lib); + builder.linkStaticLibrary(storage_utils_lib); + const lp_local_component = builder.buildLibrary('zoro::c_prod', lp_local); + return lp_local_component; +}; + +export async function deployNewPool({ + client, + token0, + token1, +}: DeployNewPoolParams) { + const lp_local_lib = build_lp_local_component(client); + const c_prod_lib = build_c_prod_component(client); + + const assets = new StorageMap(); + assets.insert( + Word.newFromFelts([ + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(BigInt(0)), + ]), + Word.newFromFelts([ + token1.faucetId.suffix(), + token1.faucetId.prefix(), + token0.faucetId.suffix(), + token0.faucetId.prefix(), + ]), + ); + + const assets_mapping_slot = StorageSlot.map('zoro::lp_local::assets_mapping', assets); + + const reserve_slot = StorageSlot.emptyValue('zoro::lp_local::reserve'); + const total_supply_slot = StorageSlot.emptyValue('zoro::lp_local::total_supply'); + const user_deposits_mapping = new StorageMap(); + user_deposits_mapping.insert( + Word.newFromFelts([ + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(BigInt(1)), + ]), + Word.newFromFelts([ + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(BigInt(1)), + ]), + ); + const user_deposits_slot = StorageSlot.map( + 'zoro::lp_local::user_deposits_mapping', + user_deposits_mapping, + ); + + const lp_local_component = AccountComponent.fromLibrary( + lp_local_lib, + [ + assets_mapping_slot, + reserve_slot, + total_supply_slot, + user_deposits_slot, + ], + ); + + const c_prod_pool_component = AccountComponent.fromLibrary(c_prod_lib, []); + + const walletSeed = new Uint8Array(32); + crypto.getRandomValues(walletSeed); + + const secretKey = AuthSecretKey.rpoFalconWithRNG(walletSeed); + const authComponent = AccountComponent.createAuthComponentFromSecretKey(secretKey); + + const contract = new AccountBuilder(walletSeed) + .accountType(AccountType.RegularAccountUpdatableCode) + .storageMode(AccountStorageMode.public()) + .withAuthComponent(authComponent) + .withComponent(lp_local_component) + .withComponent(c_prod_pool_component) + .build(); + + await client.addAccountSecretKeyToWebStore( + contract.account.id(), + secretKey, + ); + await client.syncState(); + + return { + accountId: contract.account.id(), + }; +} diff --git a/src/lib/XykDepositNote.ts b/src/lib/XykDepositNote.ts new file mode 100644 index 0000000..68a858e --- /dev/null +++ b/src/lib/XykDepositNote.ts @@ -0,0 +1,100 @@ +import { CustomTransaction } from '@demox-labs/miden-wallet-adapter'; +import { + AccountId, + FeltArray, + FungibleAsset, + MidenArrays, + Note, + NoteAssets, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteTag, + NoteType, + OutputNote, + TransactionRequestBuilder, + WebClient, +} from '@miden-sdk/miden-sdk'; + +import zoropool from '@/masm/accounts/zoropool.masm?raw'; +import DEPOSIT_SCRIPT from '@/masm/notes/xyk_deposit.masm?raw'; +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { accountIdToBech32, generateRandomSerialNumber } from './utils'; + +export interface DepositParams { + token0: TokenConfig; + token1: TokenConfig; + amount0: bigint; + amount1: bigint; + userAccountId: AccountId; + poolAccountId: AccountId; + client: WebClient; +} + +export interface SwapResult { + readonly txId: string; + readonly noteId: string; +} + +export async function compileXykDepositTransaction({ + poolAccountId, + userAccountId, + token0, + token1, + amount0, + amount1, + client, +}: DepositParams) { + const builder = client.createCodeBuilder(); + const pool_script = builder.buildLibrary('zoroswap::zoropool', zoropool); + builder.linkDynamicLibrary(pool_script); + const script = builder.compileNoteScript( + DEPOSIT_SCRIPT, + ); + + // Note should only contain the offered asset + const noteTag = NoteTag.withAccountTarget(poolAccountId); + + const metadata = new NoteMetadata( + userAccountId, + NoteType.Public, + noteTag, + ); + + const inputs = new NoteInputs( + new FeltArray([ + userAccountId.prefix(), + userAccountId.suffix(), + ]), + ); + + const asset0 = new FungibleAsset(token0.faucetId, amount0); + const asset1 = new FungibleAsset(token1.faucetId, amount1); + const noteAssets = new NoteAssets([asset0, asset1]); + + const note = new Note( + noteAssets, + metadata, + new NoteRecipient(generateRandomSerialNumber(), script, inputs), + ); + + const noteId = note.id().toString(); + + const transactionRequest = new TransactionRequestBuilder() + .withOwnOutputNotes(new MidenArrays.OutputNoteArray([OutputNote.full(note)])) + .build(); + + const tx = new CustomTransaction( + accountIdToBech32(userAccountId), + accountIdToBech32(poolAccountId), + transactionRequest, + [], + [], + ); + + return { + tx, + noteId, + note, + }; +} diff --git a/src/lib/ZoroDepositNote.ts b/src/lib/ZoroDepositNote.ts index e58c8b0..b1258e5 100644 --- a/src/lib/ZoroDepositNote.ts +++ b/src/lib/ZoroDepositNote.ts @@ -17,10 +17,10 @@ import { WebClient, } from '@miden-sdk/miden-sdk'; +import zoropool from '@/masm/accounts/zoropool.masm?raw'; +import DEPOSIT_SCRIPT from '@/masm/notes/DEPOSIT.masm?raw'; import type { TokenConfig } from '@/providers/ZoroProvider'; -import DEPOSIT_SCRIPT from './DEPOSIT.masm?raw'; import { accountIdToBech32, generateRandomSerialNumber } from './utils'; -import zoropool from './zoropool.masm?raw'; export interface DepositParams { poolAccountId: AccountId; diff --git a/src/lib/ZoroSwapNote.ts b/src/lib/ZoroSwapNote.ts index 3bf57e8..ba64f18 100644 --- a/src/lib/ZoroSwapNote.ts +++ b/src/lib/ZoroSwapNote.ts @@ -1,3 +1,4 @@ +import { CustomTransaction } from '@demox-labs/miden-wallet-adapter'; import { AccountId, Felt, @@ -15,13 +16,11 @@ import { TransactionRequestBuilder, WebClient, } from '@miden-sdk/miden-sdk'; -import { CustomTransaction } from '@demox-labs/miden-wallet-adapter'; +import zoropool from '@/masm/accounts/zoropool.masm?raw'; +import ZOROSWAP_SCRIPT from '@/masm/notes/ZOROSWAP.masm?raw'; import type { TokenConfig } from '@/providers/ZoroProvider'; import { accountIdToBech32, generateRandomSerialNumber } from './utils'; -import ZOROSWAP_SCRIPT from './ZOROSWAP.masm?raw'; - -import zoropool from './zoropool.masm?raw'; export interface SwapParams { poolAccountId: AccountId; @@ -81,9 +80,9 @@ export async function compileSwapTransaction({ new Felt(BigInt(p2idTag)), new Felt(BigInt(0)), new Felt(BigInt(0)), - userAccountId.suffix(), // beneficiary + userAccountId.suffix(), // beneficiary userAccountId.prefix(), - userAccountId.suffix(), // creator + userAccountId.suffix(), // creator userAccountId.prefix(), ]), ); diff --git a/src/lib/ZoroWithdrawNote.ts b/src/lib/ZoroWithdrawNote.ts index 6418517..6fb1c54 100644 --- a/src/lib/ZoroWithdrawNote.ts +++ b/src/lib/ZoroWithdrawNote.ts @@ -17,10 +17,10 @@ import { WebClient, } from '@miden-sdk/miden-sdk'; +import zoropool from '@/masm/accounts/zoropool.masm?raw'; +import WITHDRAW_SCRIPT from '@/masm/notes/WITHDRAW.masm?raw'; import type { TokenConfig } from '@/providers/ZoroProvider'; import { accountIdToBech32, generateRandomSerialNumber } from './utils'; -import WITHDRAW_SCRIPT from './WITHDRAW.masm?raw'; -import zoropool from './zoropool.masm?raw'; export interface WithdrawParams { poolAccountId: AccountId; diff --git a/src/utils/format.ts b/src/lib/format.ts similarity index 100% rename from src/utils/format.ts rename to src/lib/format.ts diff --git a/src/lib/poolUtils.ts b/src/lib/poolUtils.ts new file mode 100644 index 0000000..af998a1 --- /dev/null +++ b/src/lib/poolUtils.ts @@ -0,0 +1,32 @@ +import type { TokenConfig } from '@/providers/ZoroProvider'; + +const STORAGE_KEY = 'zoro-created-pools'; + +export interface CreatedPoolDraft { + id: string; + type: 'xyk'; + tokenA: Pick; + tokenB: Pick; + feeBps: number; + createdAt: number; + status: 'draft'; +} + +export function readCreatedPools() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw) as CreatedPoolDraft[]; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +export function writeCreatedPools(pools: CreatedPoolDraft[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(pools)); +} + +export function clearCreatedPools() { + writeCreatedPools([]); +} diff --git a/src/utils/shared.ts b/src/lib/shared.ts similarity index 100% rename from src/utils/shared.ts rename to src/lib/shared.ts diff --git a/src/masm/accounts/c_prod_pool.masm b/src/masm/accounts/c_prod_pool.masm new file mode 100644 index 0000000..aaa7f30 --- /dev/null +++ b/src/masm/accounts/c_prod_pool.masm @@ -0,0 +1,164 @@ +use miden::protocol::native_account +#use miden::protocol::active_account +#use miden::protocol::active_note +use miden::protocol::output_note +#use miden::protocol::asset + +use miden::core::sys + +use miden::core::math::u64 + + +use zoro::math +#use zoro::storage_utils +use zoro::lp_local + +use lp_local::set_reserves +pub use lp_local::get_reserves +pub use lp_local::get_pool_asset_ids +use lp_local::add_to_reserve_by_asset_id +use lp_local::sub_from_reserve_by_asset_id +pub use lp_local::get_reserve_by_asset_id +pub use lp_local::is_token_0 +pub use lp_local::is_token_1 + + +### STORAGE SLOTS +const ASSETS_MAPPING_SLOT = word("zoro::lp_local::assets_mapping") +const RESERVE_SLOT = word("zoro::lp_local::reserve") + + +const ERR_INSUFFICIENT_OUTPUT = "Insufficient output" +const ERR_ZERO_AMOUNT = "Zero amount" + + +### MEMORY LOCATIONS +const ASSET_IN_WORD = 20 +const ASSET_IN_ID_PREFIX = 20 +const ASSET_IN_ID_SUFFIX = 21 +const AMOUNT_IN = 23 + +const ASSET_OUT_WORD = 28 +const ASSET_OUT_ID_PREFIX = 28 +const ASSET_OUT_ID_SUFFIX = 29 +const AMOUNT_OUT = 31 + +## NOTE METADATA +const RETURN_NOTE_DETAILS_WORD = 32 +const RETURN_NOTE_TAG = 32 +const RETURN_NOTE_TYPE = 33 + +const RETURN_NOTE_RECIPIENT_WORD = 36 + +const RETURN_NOTE_ASSET_WORD = 40 +const RETURN_NOTE_ASSET_ID_PREFIX = 40 +const RETURN_NOTE_ASSET_ID_SUFFIX = 41 +const RETURN_NOTE_AMOUNT = 43 + + + + +#@todo factor out fee amount and make it configurable +@locals(6) +pub proc get_amount_out_u64(amount_in: felt, reserve_out: felt, reserve_in: felt) -> felt + # loc.0: reserve_in + # loc.1: reserve_out + # loc.2: amount_in + # loc.3: unused + # loc.4: amount_in_with_fee_high + # loc.5: amount_in_with_fee_low + + loc_store.0 loc_store.1 loc_store.2 + + # amount_in + loc_load.2 u32split + # => [amount_in_high, amount_in_low] + push.997 u32split + # => [997_high, 997_low, amount_in_high, amount_in_low] + # @note: consider overflowing mul + exec.u64::wrapping_mul + # => [amount_in_with_fee_high, amount_in_with_fee_low] + loc_store.4 loc_store.5 loc_load.5 loc_load.4 + # => [amount_in_with_fee_high, amount_in_with_fee_low] + ## reserve_out + loc_load.1 u32split + # => [reserve_out_high, reserve_out_low, amount_in_with_fee_high, amount_in_with_fee_low] + # numerator = amount_in_with_fee * reserve_out + exec.u64::wrapping_mul + # => [numerator_high, numerator_low] + # reserve_in + loc_load.0 u32split push.1000 u32split exec.u64::wrapping_mul + # => [reserve_in_scaled_high, reserve_in_scaled_low, numerator_high, numerator_low] + # amount_in_with_fee + loc_load.5 loc_load.4 + # => [amount_in_with_fee_high, amount_in_with_fee_low, reserve_in_scaled_high, reserve_in_scaled_low, numerator_high, numerator_low] + # denominator = reserve_in_scaled + amount_in_with_fee + exec.u64::wrapping_add + # => [denominator_high, denominator_low, numerator_high, numerator_low] + exec.u64::div + # => [amount_out_high, amount_out_low] + exec.math::safe_cast_u64_into_felt + # => [amount_out] + exec.sys::truncate_stack +end + + + +@locals(2) +pub proc do_swap(asset_in: word, min_asset_out: word, deadline: felt, note_tag: felt, note_type: felt, return_note_recipient: word) -> felt + # loc.0: deadline + # loc.1: amount_out + ### localy store asset in, min asset out, and deadline + mem_storew_le.ASSET_IN_WORD dropw mem_storew_le.ASSET_OUT_WORD dropw + ### localy store deadline + loc_store.0 + ### store note metadata in memory + mem_store.RETURN_NOTE_TAG mem_store.RETURN_NOTE_TYPE mem_storew_le.RETURN_NOTE_RECIPIENT_WORD dropw + mem_load.ASSET_IN_ID_SUFFIX mem_load.ASSET_IN_ID_PREFIX exec.get_reserve_by_asset_id + # => [reserve_in] + mem_load.ASSET_OUT_ID_SUFFIX mem_load.ASSET_OUT_ID_PREFIX exec.get_reserve_by_asset_id + # => [reserve_out, reserve_in] + mem_load.AMOUNT_IN + # => [amount_in, reserve_out, reserve_in] + exec.get_amount_out_u64 loc_store.1 loc_load.1 + # => [amount_out] + mem_load.AMOUNT_OUT + # => [min_amount_out, amount_out] + gte + if.true # sufficient amount out + ### send asset out to user + padw mem_loadw_le.ASSET_OUT_WORD + mem_storew_le.RETURN_NOTE_ASSET_WORD dropw + ### add ASSEET IN to pool + padw mem_loadw_le.ASSET_IN_WORD + exec.native_account::add_asset dropw + # => [] + ### update reserve + + else # insufficient amount out + ### send asset in back to user + padw mem_loadw_le.ASSET_IN_WORD + mem_storew_le.RETURN_NOTE_ASSET_WORD dropw + end + exec._create_p2id_note + + + exec.sys::truncate_stack +end + + +#### HELPER PROCEDURES +@locals(1) +proc _create_p2id_note() + # loc.0: note_id + padw mem_loadw_le.RETURN_NOTE_RECIPIENT_WORD mem_load.RETURN_NOTE_TYPE mem_load.RETURN_NOTE_TAG + exec.output_note::create loc_store.0 # @todo maybe don't even store at local + # => [] + + padw mem_loadw_le.RETURN_NOTE_ASSET_WORD + exec.native_account::remove_asset + # => [ASSET] + loc_load.0 movdn.4 + # => [ASSET1_OUT, note_id] + exec.output_note:: add_asset +end diff --git a/src/masm/accounts/lp_local.masm b/src/masm/accounts/lp_local.masm new file mode 100644 index 0000000..abd91bf --- /dev/null +++ b/src/masm/accounts/lp_local.masm @@ -0,0 +1,606 @@ +use miden::core::math::u64 +use miden::protocol::native_account +use miden::protocol::active_account +use miden::protocol::output_note +use miden::core::sys +use miden::protocol::account_id + +use zoro::math +use zoro::storage_utils + +const MINIMUM_LIQUIDITY = 100 + +### MEMORY LOCATIONS +const DYNAMIC_PROC_ADDR = 4 + +const USER_ACCOUNT_ID_WORD = 20 +const USER_ACCOUNT_ID_PREFIX = 20 +const USER_ACCOUNT_ID_SUFFIX = 21 + +const NOTE_DETAILS_WORD = 24 +const NOTE_TAG = 24 +const NOTE_TYPE = 25 + +const NOTE_RECIPIENT_WORD = 28 + +### ERROR CODES +const ERR_ZERO_ADDRESS = "Zero address" +const ERR_UNKNOWN_ASSET = "Unknown asset" + +### STORAGE SLOTS +const ASSETS_MAPPING_SLOT = word("zoro::lp_local::assets_mapping") +const RESERVE_SLOT = word("zoro::lp_local::reserve") +const TOTAL_SUPPLY_SLOT = word("zoro::lp_local::total_supply") +const USER_DEPOSITS_MAPPING_SLOT = word("zoro::lp_local::user_deposits_mapping") + +################# +#! Computes the LP amount out for a deposit of amount_0 and amount_1. +#! For initial deposit (the total supply is 0), it subracts the minimum liquidity. +#! Inputs: [total_supply, amount_0, amount_1, reserve_0, reserve_1] +#! Outputs: [lp_amount_out] +@locals(5) +pub proc get_lp_amount_out(total_supply: felt, amount_0: felt, amount_1: felt, reserve_0: felt, reserve_1: felt) -> felt + # loc.0: total_supply + # loc.1: amount_0 + # loc.2: amount_1 + # loc.3: reserve_0 + # loc.4: reserve_1 + loc_store.0 loc_store.1 loc_store.2 loc_store.3 loc_store.4 + # => [] + ### if total_supply == 0 + loc_load.0 eq.0 + if.true + ### initial deposit: - MINIMUM_LIQUIDITY + loc_load.1 loc_load.2 mul exec.math::sqrt + # => [lp_amount] + push.MINIMUM_LIQUIDITY exec.math::safe_sub + # => [lp_amount_out] + ### return lp_amount_out + else + ### subsequent deposits: min(lp_amount_0, lp_amount_1) + ### lp_amount_0 = amount_0 * total_supply / reserve_0 + loc_load.1 u32split loc_load.0 u32split + # => [amount_0_high, amount_0_low, total_supply_high, total_supply_low] + exec.u64::wrapping_mul + # => [numerator_high, numerator_low] + loc_load.3 u32split + # => [reserve_0_hi, reserve_0_low, numerator_hi, numerator_low] + exec.u64::div + # => [lp_amount_0_high, lp_amount_0_low] + loc_load.2 u32split loc_load.0 u32split exec.u64::wrapping_mul loc_load.4 u32split exec.u64::div + # => [lp_amount_1_high, lp_amount_1_low, lp_amount_0_high, lp_amount_0_low] + exec.u64::min + # => [lp_amount_out_high, lp_amount_out_low] + exec.math::safe_cast_u64_into_felt + # => [lp_amount_out] + ### return lp_amount_out + end + + exec.sys::truncate_stack +end + +@locals(5) +pub proc simulate_withdraw#(total_suppply: felt, lp_amount: felt, reserve_0: felt, reserve_1: felt) -> (felt, felt) + # loc.0: total_suppply + # loc.1: lp_amount + # loc.2: reserve_0 + # loc.3: reserve_1 + # loc.4: amount_0_out + loc_store.0 loc_store.1 loc_store.2 loc_store.3 + # => [] + ### amount0 = lp_amount.mul(reserve) / total_suppply; // using balances ensures pro-rata distribution + loc_load.1 u32split loc_load.2 u32split exec.u64::wrapping_mul loc_load.0 u32split exec.u64::div + # => [amount_0_out_high, amount_0_out_low] + exec.math::safe_cast_u64_into_felt loc_store.4 + loc_load.1 u32split loc_load.3 u32split exec.u64::wrapping_mul loc_load.0 u32split exec.u64::div + # => [amount_1_out_high, amount_1_out_low] + exec.math::safe_cast_u64_into_felt + # => [amount_1_out] + loc_load.4 + # => [amount_0_out, amount_1_out] + ### truncate stack + exec.sys::truncate_stack +end + +proc assert_non_zero_address(prefix: felt, suffix: felt) + neq.0 swap.1 neq.0 + # => [is_suffix_non_zero, is_prefix_non_zero] + or assert.err=ERR_ZERO_ADDRESS +end +proc get_current_user_deposit_key() -> Word + mem_load.USER_ACCOUNT_ID_SUFFIX mem_load.USER_ACCOUNT_ID_PREFIX + exec.assert_non_zero_address + mem_load.USER_ACCOUNT_ID_SUFFIX mem_load.USER_ACCOUNT_ID_PREFIX + # => [prefix, suffix] + exec.get_user_deposit_key + # => [USER_DEPOSIT_KEY] + exec.sys::truncate_stack +end + +proc get_user_deposit_key#(prefix: felt, suffix: felt) -> Word + push.0.0 movup.3 movup.3 + # => [prefix, suffix, 0 , 0 ] == [KEY] +end + + + +pub proc total_supply#() -> felt + push.TOTAL_SUPPLY_SLOT[0..2] exec.active_account::get_item + drop drop drop + + exec.sys::truncate_stack +end + +pub proc get_reserves#() -> (felt, felt) + push.RESERVE_SLOT[0..2] exec.active_account::get_item + drop drop +end + +pub proc set_reserves#(reserve_0: felt, reserve_1: felt) + ### amount validation vs vault + push.0.0push.RESERVE_SLOT[0..2] exec.native_account::set_item + dropw +end + +pub proc get_pool_asset_ids() -> word + push.0.0.0.0 push.ASSETS_MAPPING_SLOT[0..2] exec.active_account::get_map_item + exec.sys::truncate_stack +end + +#! Returns user deposit (least significant felt) for verification in fuzz tests. +#! Inputs: [prefix, suffix] +pub proc get_user_deposit#(prefix: felt, suffix: felt) -> felt + exec.get_user_deposit_key + push.USER_DEPOSITS_MAPPING_SLOT[0..2] exec.active_account::get_map_item + drop drop drop + exec.sys::truncate_stack +end + +#### PROCEDURES BY ASSET ID #### +@locals(2) +pub proc get_reserve_by_asset_id(asset_id_prefix: felt, asset_id_suffix: felt) -> felt + exec.get_asset_index + eq.0 + if.true # asset 0 + exec.get_reserves + # => [reserve_0, reserve_1] + swap drop + # => [reserve_0] + else # asset 1 + exec.get_reserves + # => [reserve_0, reserve_1] + drop + # => [reserve_1] + end + exec.sys::truncate_stack +end + + +pub proc is_token_0(asset_id_prefix: felt, asset_id_suffix: felt) -> i1 + exec.get_pool_asset_ids movup.2 drop movup.2 drop + # => [asset_0_id_prefix, asset_0_id_suffix, asset_id_prefix, asset_id_suffix] + + exec.account_id::is_equal + # => + exec.sys::truncate_stack +end +pub proc is_token_1(asset_id_prefix: felt, asset_id_suffix: felt) -> i1 + exec.get_pool_asset_ids drop drop + # => [asset_1_id_prefix, asset_1_id_suffix, asset_id_prefix, asset_id_suffix] + exec.account_id::is_equal + # => + exec.sys::truncate_stack +end + +@locals(2) +pub proc get_asset_index(asset_id_prefix: felt, asset_id_suffix: felt) -> felt + # loc.0: asset_id_prefix + # loc.1: asset_id_suffix + loc_store.0 loc_store.1 + loc_load.1 loc_load.0 + # => [asset_id_suffix, asset_id_prefix] + exec.is_token_0 + if.true + ### index = 0 + push.0 + # => [0] + else + loc_load.1 loc_load.0 + # => [asset_id_suffix, asset_id_prefix] + exec.is_token_1 + # => [is_one] + ### index not found + assert.err=ERR_UNKNOWN_ASSET + ### index = 1 + push.1 + # => [1] + end + exec.sys::truncate_stack +end + + +@locals(2) +pub proc add_to_reserves#(amount_0: felt, amount_1: felt) + # loc.0: amount_0 + # loc.1: amount_1 + loc_store.0 loc_store.1 + # => [] + exec.get_reserves + # => [reserve_0, reserve_1] + loc_load.0 exec.math::safe_add + # => [new_reserve_0, reserve_1] + swap + loc_load.1 exec.math::safe_add + # => [new_reserve_1, new_reserve_0] + swap + # => [new_reserve_0, new_reserve_1] + exec.set_reserves +end + +@locals(2) +proc sub_from_reserves#(amount_0: felt, amount_1: felt) + # loc.0: amount_0 + # loc.1: amount_1 + loc_store.0 loc_store.1 + # => [] + exec.get_reserves + # => [reserve_0, reserve_1] + loc_load.0 exec.math::safe_sub + # => [new_reserve_0, reserve_1] + swap + loc_load.1 exec.math::safe_sub + # => [new_reserve_1, new_reserve_0] + swap + # => [new_reserve_0, new_reserve_1] + exec.set_reserves +end + +pub proc add_to_reserve_by_asset_id#(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) +end +proc sub_from_reserve_by_asset_id#(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) +end + +@locals(3) +proc mint#(amount: felt, beneficiary_prefix: felt, beneficiary_suffix: felt) + # loc.0: amount + # loc.1: beneficiary_prefix + # loc.2: beneficiary_suffix + loc_store.0 loc_store.1 loc_store.2 + # => [] + push.0.0.0 loc_load.0 + # => [AMOUNT] + ### add to mapping USER_DEPOSITS_MAPPING_SLOT + loc_load.2 loc_load.1 exec.get_user_deposit_key + # => [USER_DEPOSIT_KEY, AMOUNT] + push.USER_DEPOSITS_MAPPING_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, USER_DEPOSIT_KEY, AMOUNT] + exec.storage_utils::add_to_map_item + ### updating total supply + loc_load.0 push.TOTAL_SUPPLY_SLOT[0..2] exec.storage_utils::add_to_storage_item + drop +end + +@locals(3) +proc burn#(amount: felt, own_prefix: felt, own_suffix: felt) + # loc.0: amount + # loc.1: owner_prefix + # loc.2: own_suffix + loc_store.0 loc_store.1 loc_store.2 + # => [] + push.0.0.0 loc_load.0 + # => [AMOUNT] + ### sub from mapping USER_DEPOSITS_MAPPING_SLOT + loc_load.2 loc_load.1 exec.get_user_deposit_key + # => [USER_DEPOSIT_KEY, AMOUNT] + push.USER_DEPOSITS_MAPPING_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, USER_DEPOSIT_KEY, AMOUNT] + exec.storage_utils::sub_from_map_item + drop + ### updating total supply + loc_load.0 push.TOTAL_SUPPLY_SLOT[0..2] exec.storage_utils::sub_from_storage_item + drop +end + + + +#! add new deposit: receive asset, update deposit mapping +#! +#! Inputs: [ASSET0, ASSET1, user_id_prefix, user_id_suffix] +#! Outputs: [share_amount_out] +#! +@locals(9) +pub proc deposit + # loc.0: ASSET0 + # loc.4: ASSET1 + # loc.8: lp_amount_out + loc_storew_le.0 dropw loc_storew_le.4 dropw mem_store.USER_ACCOUNT_ID_PREFIX mem_store.USER_ACCOUNT_ID_SUFFIX + ### get_lp_amount_out (total_supply: felt, amount_0: felt, amount_1: felt, reserve_0: felt, reserve_1: felt) + exec.get_reserves + # => [reserve_0, reserve_1] + loc_load.7 loc_load.3 + # => [amount_0, amount_1, reserve_0, reserve_1] + exec.total_supply + # => [total_supply, amount_0, amount_1, reserve_0, reserve_1] + exec.get_lp_amount_out + # => [lp_amount_out] + loc_store.8 + ### check if initial deposit + exec.total_supply eq.0 + if.true + ### burn minimum liquidity + push.0.0 push.MINIMUM_LIQUIDITY + exec.mint + end + ### mint to user + + mem_load.USER_ACCOUNT_ID_SUFFIX mem_load.USER_ACCOUNT_ID_PREFIX + loc_load.8 + # => [lp_amount_out,user_id_prefix, user_id_suffix] + exec.mint + + ### receive assets + ### reserve needs to be updated + #padw padw padw loc_loadw_be.0 exec.receive_asset + #padw padw padw loc_loadw_be.4 exec.receive_asset + padw loc_loadw_le.0 exec.native_account::add_asset dropw + padw loc_loadw_le.4 exec.native_account::add_asset dropw + ## make sure asset order correct + loc_load.7 loc_load.3 + # => [amount_0, amount_1] + exec.add_to_reserves +end + + +#! add new deposit: receive asset, update deposit mapping +#! amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution +#! amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution +#! Inputs: [LP_AMOUNT user_id_prefix, user_id_suffix] +#! Outputs: [amount_0_out, amount_1_out] +#! +@locals(6) +pub proc withdraw(lp_amount: word, user_id_prefix: felt, user_id_suffix: felt, note_tag: felt, note_type: felt, return_note_recipient: word) -> (felt, felt) + # loc.0: LP_AMOUNT = EMPTY, EMPTY, EMPTY, LP_AMOUNT loc.3 lp_amount + # loc.4: amount_0_out + # loc.5: amount_1_out + ## LP_AMOUNT to local + loc_storew_le.0 dropw + ## user, note tag and type tomemory + mem_store.USER_ACCOUNT_ID_PREFIX mem_store.USER_ACCOUNT_ID_SUFFIX mem_store.NOTE_TAG mem_store.NOTE_TYPE + ## RECIPIENT to memroy + mem_storew_le.NOTE_RECIPIENT_WORD dropw + # => [] + ### get_lp_amount_out (total_supply: felt, amount_0: felt, amount_1: felt, reserve_0: felt, reserve_1: felt) + exec.get_reserves + # => [reserve_0, reserve_1] + loc_load.3 + # => [lp_amount, reserve_0, reserve_1] + exec.total_supply + ## => [total_supply, lp_amount, reserve_0, reserve_1] + exec.simulate_withdraw + ## => [amount_0_out, amount_1_out] + loc_store.4 loc_store.5 + loc_load.5 loc_load.4 + ## => [amount_0_out, amount_1_out] + exec.create_withdraw_return_note + ## => [note_id] + drop + + #### burn: updates user_deposits_mapping and total_supply + mem_load.USER_ACCOUNT_ID_SUFFIX mem_load.USER_ACCOUNT_ID_PREFIX loc_load.3 + ## => [lp_amount, user_id_prefix, user_id_suffix] + exec.burn + # + loc_load.5 loc_load.4 + ## => [amount_0_out, amount_1_out] + exec.sub_from_reserves + # => [] + loc_load.5 loc_load.4 + exec.sys::truncate_stack + +end + + +### MEMORY SETTING PROCEDURES aka. global variables +proc mem_setw_recipient(recipient: word) + mem_storew_le.NOTE_RECIPIENT_WORD dropw +end +proc mem_set_note_tag(note_tag: felt) + mem_store.NOTE_TAG +end +proc mem_set_note_type(note_type: felt) + mem_store.NOTE_TYPE +end +proc mem_set_user_id(user_id_prefix: felt, user_id_suffix: felt) + mem_store.USER_ACCOUNT_ID_PREFIX mem_store.USER_ACCOUNT_ID_SUFFIX +end + +proc mem_get_user_id() -> (felt, felt) + mem_load.USER_ACCOUNT_ID_PREFIX mem_load.USER_ACCOUNT_ID_SUFFIX + exec.sys::truncate_stack +end +proc mem_get_note_tag() -> felt + mem_load.NOTE_TAG + exec.sys::truncate_stack +end +proc mem_get_note_type() -> felt + mem_load.NOTE_TYPE + exec.sys::truncate_stack +end +proc mem_get_recipient() -> word + padw mem_load.NOTE_RECIPIENT_WORD + exec.sys::truncate_stack +end + +@locals(8) +proc create_withdraw_return_note(amount_0_out: felt, amount_1_out: felt) -> felt + # loc.0: amount_0_out + # loc.1: amount_1_out + # loc.2: note_id + # loc.3: asset_0_id_prefix + # loc.4: asset_0_id_suffix + + loc_store.0 loc_store.1 + padw mem_loadw_le.NOTE_RECIPIENT_WORD mem_load.NOTE_TYPE mem_load.NOTE_TAG + exec.output_note::create + # => [note_id] + loc_store.2 + # => [] + ### get assets ids + push.0.0.0.0 push.ASSETS_MAPPING_SLOT[0..2] exec.active_account::get_map_item + # => [asset_0_id_prefix, asset_0_id_suffix, asset_1_id_prefix, asset_1_id_suffix] + loc_store.3 loc_store.4 + push.0 movdn.2 loc_load.1 movdn.3 + # => [ASSET1_OUT] + exec.native_account::remove_asset + # => [ASSET1_OUT] + loc_load.2 movdn.4 + # => [ASSET1_OUT, note_id] + exec.output_note::add_asset + # => [] + loc_load.0 + # => [asset_0_out] + push.0 loc_load.4 loc_load.3 + # => [ASSET0_OUT] + exec.native_account::remove_asset + # => [ASSET0_OUT] + loc_load.2 movdn.4 + # => [ASSET0_OUT, note_id] + exec.output_note::add_asset + + # => [] +end + + +# # => [amount_0_out, amount_1_out] +# loc_store.5 loc_store.6 +# ### check if initial deposit +# +# ### mint to user +# +# mem_load.USER_ACCOUNT_ID_SUFFIX mem_load.USER_ACCOUNT_ID_PREFIX +# loc_load.3 +# # => [lp_amount,user_id_prefix, user_id_suffix] +# exec.burn +# +# ### add assets back to user +# ### reserve needs to be updated +# #padw padw padw loc_loadw_be.0 exec.receive_asset +# #padw padw padw loc_loadw_be.4 exec.receive_asset +# #@todo - move to note valdiate +# #@todo - also simulate_withdraw a +# padw loc_loadw_le.4 exec.native_account::add_asset dropw +# padw loc_loadw_le.5 exec.native_account::add_asset dropw +# ## make sure asset order correct +# loc_load.7 loc_load.3 +# # => [amount_0, amount_1] +# exec.add_to_reserve +#end + +##! Inputs: [user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET_OUT, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage] +#pub proc withdraw +# dupw +# # => [user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET_OUT, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# dupw.2 +# # => [ASSET_OUT, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET_OUT, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# exec.get_user_deposit_key +# # => [KEY, lp_withdraw_amount, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0 ] +# mem_storew_be.WITHDRAW_KEY_LOC +# exec.get_user_deposit +# # => [user_lp_shares, lp_withdraw_amount, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# swap sub +# # => [new_user_lp_shares, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0 ] +# dup +# # => [new_user_lp_shares, new_user_lp_shares, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# padw mem_loadw_be.WITHDRAW_KEY_LOC +# exec.get_user_deposit +# # => [user_lp_shares, new_user_lp_shares, new_user_lp_shares, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# lt assert.err=ERR_LP_WITHDRAW_AMOUNT_EXCEEDS_USER_LP_SHARES +# # => [new_user_lp_shares, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# +# # store new user lp shares +# push.0.0.0 +# # => [NEW_USER_LP_SHARES, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# padw mem_loadw_be.WITHDRAW_KEY_LOC +# push.USER_DEPOSITS_MAPPING_SLOT[0..2] +# # => [slot_id_prefix, slot_id_suffix, KEY, NEW_USER_LP_SHARES] +# exec.native_account::set_map_item +# dropw drop +# # => [user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# drop drop swap drop +# mem_store.LP_SHARES_AMOUNT_ADDR +# +# +# mem_load.LP_SHARES_AMOUNT_ADDR push.0.0.0 +# padw mem_loadw_be.WITHDRAW_KEY_LOC +# exec.get_lp_shares_total_supply_key +# # => [KEY, SHARE_AMOUNT] +# push.USER_DEPOSITS_MAPPING_SLOT[0..2] +# # => [slot_id_prefix, slot_id_suffix, KEY, SHARE_AMOUNT] +# exec.sub_from_map_item +# # => [ ASSET_OUT, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] +# exec.move_asset_to_note +# dropw dropw dropw +#end + + +##! Atomically increments a storage map entry: map[KEY] += VALUE. +##! Reads the current value via get_map_item, adds VALUE, writes back via set_map_item. +##! +##! Inputs: [slot_id_prefix, slot_id_suffix, KEY, VALUE] +##! Outputs: [] +#proc add_to_map_item +# # Save slot ID to memory +# mem_store.HELPER_SLOT_ID_PREFIX_LOC +# mem_store.HELPER_SLOT_ID_SUFFIX_LOC +# # => [KEY, VALUE] +# +# # Duplicate KEY for second call +# dupw +# # => [KEY, KEY, VALUE] +# +# # Load slot ID and get current map value +# mem_load.HELPER_SLOT_ID_SUFFIX_LOC +# mem_load.HELPER_SLOT_ID_PREFIX_LOC +# # => [slot_id_prefix, slot_id_suffix, KEY, KEY, VALUE] +# exec.active_account::get_map_item drop drop drop +# # => [old_amount, KEY, VALUE] +# +# # Add new amount to old amount +# movup.8 add +# # => [new_amount, KEY, 0, 0, 0] +# movdn.7 +# # => [KEY, 0, 0, 0, new_amount] +# # => [KEY, NEW_VALUE] +# +# # Load slot ID and set map item +# mem_load.HELPER_SLOT_ID_SUFFIX_LOC +# mem_load.HELPER_SLOT_ID_PREFIX_LOC +# # => [slot_id_prefix, slot_id_suffix, KEY, NEW_VALUE] +# exec.native_account::set_map_item dropw +# # => [] +#end + + +#! Adds the provided asset to the active account. +#! +#! Inputs: [ASSET, pad(4, pad(8)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - ASSET is the asset to be received, can be fungible or non-fungible +#! +#! Panics if: +#! - the same non-fungible asset already exists in the account. +#! - adding a fungible asset would result in amount overflow, i.e., +#! the total amount would be greater than 2^63. +#! +#! Invocation: call +#pub proc receive_asset +# exec.native_account::add_asset +# # => [ASSET', reserve, reserve_with_slippage, liabilities, 0, pad(8)] +# +# procref.validate_and_update_state mem_storew_be.DYNAMIC_PROC_ADDR dropw push.DYNAMIC_PROC_ADDR +# dynexec +# +# # => [pad(16)] +#end diff --git a/src/masm/accounts/math.masm b/src/masm/accounts/math.masm new file mode 100644 index 0000000..1c0723c --- /dev/null +++ b/src/masm/accounts/math.masm @@ -0,0 +1,177 @@ +use miden::core::math::u64 +use miden::core::sys + +const ERR_UNDERFLOW = "Underflow" +const ERR_OVERFLOW = "Overflow" + + +const U32_MAX = 4294967295 +const FELT_MAX = 0xFFFFFFFF00000000 +const U32_BOUND = 0x100000000 + + +########## math +@locals(4) +pub proc sqrt_u32(n: u32) -> u32 # floor(sqrt(n)) via bit-by-bit construction. + # loc.0: n loc.1: result loc.2: bit loc.3: tmp + loc_store.0 # save n + push.0 loc_store.1 # result starts as 0 + push.0x40000000 loc_store.2 # bit starts as 1<<30 + push.0 loc_store.3 # tmp starts as 0 + + repeat.16 + loc_load.1 loc_load.2 u32wrapping_add loc_store.3 # tmp = result + bit + + loc_load.0 loc_load.3 u32gte # if n >= tmp + if.true + loc_load.0 loc_load.3 u32wrapping_sub loc_store.0 # n = n - tmp + loc_load.1 u32shr.1 loc_load.2 u32wrapping_add loc_store.1 # result = (result >> 1) + bit + else + loc_load.1 u32shr.1 loc_store.1 # result = result >> 1 + end + loc_load.2 u32shr.2 loc_store.2 # bit = bit >> 2 + end + + loc_load.1 + exec.sys::truncate_stack +end + + +@locals(8) +pub proc sqrt(n: felt) -> felt + # floor(sqrt(n)) via bit-by-bit construction. + # For any u32 candidate c, c^2 < field prime p, so felt mul is exact. + # + # loc.0: n_hi loc.1: n_lo loc.2: result_hi loc.3: result_lo + # loc.4: bit_hi loc.5: bit_lo loc.6: tmp_hi loc.7: tmp_lo + u32split loc_store.0 loc_store.1 + push.0.0 loc_store.2 loc_store.3# result starts as 0 + push.0x4000000000000000 u32split loc_store.4 loc_store.5 # bit starts as 1<<62 + push.0.0 loc_store.6 loc_store.7 # tmp starts as 0 + + repeat.32 + ### tmp = result + bit + loc_load.3 loc_load.2 + # => [res_hi, res_lo] + loc_load.5 loc_load.4 + # => [bit_hi, bit_lo, res_hi, res_lo] + exec.u64::wrapping_add loc_store.6 loc_store.7 + + ### compare n >= result + bit (n >= tmp) + loc_load.1 loc_load.0 + # => [n_hi, n_lo] + loc_load.7 loc_load.6 + # => [tmp_hi, tmp_lo, n_hi, n_lo] + exec.u64::gte + if.true + ### n = n - (result + bit) + ### n = n - tmp + loc_load.1 loc_load.0 + # => [n_hi, n_lo] + loc_load.7 loc_load.6 + # => [tmp_hi, tmp_lo, n_hi, n_lo] + exec.u64::wrapping_sub loc_store.0 loc_store.1 + + ### result = result + bit + loc_load.5 loc_load.4 + # => [bit_hi, bit_lo] + loc_load.3 loc_load.2 push.1 exec.u64::shr + # => [res_hi, res_lo, bit_hi, bit_lo] + exec.u64::wrapping_add loc_store.2 loc_store.3 + + else + ### result = result >> 1 + loc_load.3 loc_load.2 push.1 exec.u64::shr loc_store.2 loc_store.3 + + end + + ### bit = bit >> 2 + loc_load.5 loc_load.4 + # => [bit_hi, bit_lo] + push.2 exec.u64::shr loc_store.4 loc_store.5 + + end + ### return result + loc_load.3 loc_load.2 + exec.safe_cast_u64_into_felt + + # => [result] + exec.sys::truncate_stack +end + +@locals(3) +pub proc safe_cast_u64_into_felt(a: u64) -> felt + ### loc.0: a_hi + ### loc.1: a_hi_scaled + ### loc.2: res + # => [a_hi, a_lo] + loc_store.0 loc_load.0 + mul.U32_BOUND loc_store.1 + loc_load.1 loc_load.0 gte assert.err=ERR_OVERFLOW + # => [a_lo] + loc_load.1 + # => [a_hi_scaled, a_lo] + add loc_store.2 + ### if res < a_hi_scaled => overflow + loc_load.2 loc_load.1 + # => [a_hi_scaled, res] + gte assert.err=ERR_OVERFLOW + loc_load.2 +end + + + +@locals(2) +pub proc safe_sub(b: felt, a: felt) -> felt + # loc.0: a + # loc.1: res + swap loc_store.0 + loc_load.0 swap + sub loc_store.1 + loc_load.1 loc_load.0 + # [a, res] + lte assert.err=ERR_UNDERFLOW + loc_load.1 +end + +@locals(2) +pub proc safe_add(b: felt, a: felt) -> felt + # loc.0: b + # loc.1: res + loc_store.0 + loc_load.0 + add loc_store.1 + loc_load.1 loc_load.0 + # [a, res] + gte assert.err=ERR_OVERFLOW + loc_load.1 +end + + + +################# +#@locals(5) +#pub proc get_lp_amount_out(total_supply: felt, amount_0: felt, amount_1: felt, reserve_0: felt, reserve_1: felt) -> felt +# # loc.0: total_supply +# # loc.1: amount_0 +# # loc.2: amount_1 +# # loc.3: reserve_0 +# # loc.4: reserve_1 +# loc_store.0 +# loc_store.1 +# loc_store.2 +# loc_store.3 +# loc_store.4 +# # => [] +# # if total_supply == 0 +# loc_load.0 eq.0 +# if.true +# loc_load.1 loc_load.2 mul sqrt push.MINIMUM_LIQUIDITY safe_sub +# else +# loc_load.1 u32split loc_load.0 u32split exec.u64::wrapping_mul loc_load.3 u32split exec.u64::div +# # => [lp_amount_variant_0_high, lp_amount_variant_0_low] +# loc_load.2 u32split loc_load.0 u32split exec.u64::wrapping_mul loc_load.4 u32split exec.u64::div +# # => [lp_amount_variant_1_high, lp_amount_variant_1_low, lp_amount_variant_0_high, lp_amount_variant_0_low] +# end +#end + diff --git a/src/masm/accounts/storage_utils.masm b/src/masm/accounts/storage_utils.masm new file mode 100644 index 0000000..aa9344d --- /dev/null +++ b/src/masm/accounts/storage_utils.masm @@ -0,0 +1,134 @@ +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::sys + +use zoro::math + + +@locals(3) +pub proc add_to_storage_item(slot_id_prefix: felt, slot_id_suffix: felt, increment_by: felt) -> felt + # loc.0: slot_id_prefix + # loc.1: slot_id_suffix + # loc.2: new_value + loc_store.0 loc_store.1 + # => [increment_by] + loc_load.1 loc_load.0 exec.active_account::get_item + drop drop drop + # => [current_value, increment_by] + exec.math::safe_add + # => [new_value] + loc_store.2 + ### get current value + loc_load.2 push.0.0.0 + # => [NEW_VALUE] + loc_load.1 loc_load.0 exec.native_account::set_item + dropw + # => [] + loc_load.2 + # => [new_value] + exec.sys::truncate_stack +end + +@locals(3) +pub proc sub_from_storage_item(slot_id_prefix: felt, slot_id_suffix: felt, decrement_by: felt) -> felt + # loc.0: slot_id_prefix + # loc.1: slot_id_suffix + # loc.2: new_value + loc_store.0 loc_store.1 + # => [sub_by] + loc_load.1 loc_load.0 exec.active_account::get_item + drop drop drop + # => [current_value, sub_by] + swap + exec.math::safe_sub + # => [new_value] + loc_store.2 + ### get current value + loc_load.2 push.0.0.0 + # => [NEW_VALUE] + loc_load.1 loc_load.0 exec.native_account::set_item + dropw + # => [] + loc_load.2 + # => [new_value] + exec.sys::truncate_stack +end + + + + +#! Atomically increments a storage map entry: map[KEY] += VALUE. +#! Reads the current value via get_map_item, adds VALUE, writes back via set_map_item. +#! +#! Inputs: [slot_id_prefix, slot_id_suffix, KEY, VALUE] +#! Outputs: [] +@locals(9) +pub proc sub_from_map_item (slot_id_prefix: felt, slot_id_suffix: felt, key: word, sub_by: felt) -> felt + # loc.0: slot_id_prefix + # loc.1: slot_id_suffix + # loc.4-7: key + # loc.8: new_value + ### save inputs + loc_store.0 loc_store.1 loc_storew_be.4 movup.4 loc_store.8 + # => [KEY] + loc_load.1 loc_load.0 + # => [slot_id_prefix, slot_id_suffix, KEY] + exec.active_account::get_map_item + drop drop drop + # => [old_value] + loc_load.8 + # => [sub_by,old_value] + exec.math::safe_sub + # => [new_value] + loc_store.8 + + ### set + loc_load.8 padw loc_loadw_be.4 loc_load.1 loc_load.0 exec.set_map_item + exec.sys::truncate_stack + # => [old_value] +end + +@locals(9) +pub proc add_to_map_item #(slot_id_prefix: felt, slot_id_suffix: felt, key: Word, increment_by: felt) -> felt + # loc.0: slot_id_prefix + # loc.1: slot_id_suffix + # loc.4-7: key + # loc.8: new_value + ### save inputs + loc_store.0 loc_store.1 loc_storew_be.4 movup.4 loc_store.8 + # => [KEY] + loc_load.1 loc_load.0 + # => [slot_id_prefix, slot_id_suffix, KEY] + exec.active_account::get_map_item + drop drop drop + # => [old_value] + loc_load.8 + # => [old_value] + exec.math::safe_add + # => [new_value] + loc_store.8 + + ### set + loc_load.8 padw loc_loadw_be.4 loc_load.1 loc_load.0 exec.set_map_item + exec.sys::truncate_stack + # => [old_value] +end + + +@locals(9) +pub proc set_map_item #(slot_id_prefix: felt, slot_id_suffix: felt, key: Word, value: felt) -> felt + # loc.0: slot_id_prefix + # loc.1: slot_id_suffix + # loc.4-7: key + # loc.8: value + loc_store.0 loc_store.1 loc_storew_be.4 dropw loc_store.8 + ### fill out value with 0 to full word + loc_load.8 push.0.0.0 + ### load slot id and key + padw loc_loadw_be.4 loc_load.1 loc_load.0 + exec.native_account::set_map_item + ### remove trailing zeros + dropw + loc_load.8 +end + diff --git a/src/lib/zoropool.masm b/src/masm/accounts/zoropool.masm similarity index 100% rename from src/lib/zoropool.masm rename to src/masm/accounts/zoropool.masm diff --git a/src/lib/DEPOSIT.masm b/src/masm/notes/DEPOSIT.masm similarity index 100% rename from src/lib/DEPOSIT.masm rename to src/masm/notes/DEPOSIT.masm diff --git a/src/lib/WITHDRAW.masm b/src/masm/notes/WITHDRAW.masm similarity index 100% rename from src/lib/WITHDRAW.masm rename to src/masm/notes/WITHDRAW.masm diff --git a/src/lib/ZOROSWAP.masm b/src/masm/notes/ZOROSWAP.masm similarity index 100% rename from src/lib/ZOROSWAP.masm rename to src/masm/notes/ZOROSWAP.masm diff --git a/src/masm/notes/xyk_deposit.masm b/src/masm/notes/xyk_deposit.masm new file mode 100644 index 0000000..3998040 --- /dev/null +++ b/src/masm/notes/xyk_deposit.masm @@ -0,0 +1,24 @@ +use miden::protocol::active_note +use zoro::lp_local +use miden::core::sys + + +const EXPECTED_NUM_ASSETS = 2 +const ERR_INVALID_NUM_ASSETS = "Invalid number of assets" + + +begin + # Load assets from note (dest_ptr=0) + push.0 exec.active_note::get_assets + # => [num_assets, 0] + swap drop + # => [num_assets] + push.EXPECTED_NUM_ASSETS eq assert.err=ERR_INVALID_NUM_ASSETS + # => [] + exec.active_note::get_sender + # => [sender_prefix, sender_suffix] + padw mem_loadw_be.0 padw mem_loadw_be.4 + # => [ASSET0, ASSET1, sender_prefix, sender_suffix ] + call.lp_local::deposit + exec.sys::truncate_stack +end diff --git a/src/masm/notes/xyk_swap.masm b/src/masm/notes/xyk_swap.masm new file mode 100644 index 0000000..31b10d0 --- /dev/null +++ b/src/masm/notes/xyk_swap.masm @@ -0,0 +1,42 @@ +use miden::protocol::active_note +use zoro::c_prod_pool +use miden::core::sys + +const EXPECTED_NUM_ASSETS = 1 +const ERR_INVALID_NUM_ASSETS = "Expected exactly 1 asset for swap" + +#! Note inputs layout (12 felts stored at dest_ptr=0): +#! mem[0..3] = [0, 0, 0, min_amount_out] → MIN_ASSET_OUT word +#! mem[4..7] = [deadline, note_tag, note_type, 0] +#! mem[8..11] = [r0, r1, r2, r3] → RECIPIENT digest + +begin + exec.active_note::get_inputs + # => [num_inputs, dest_ptr] + drop drop + + # Push RECIPIENT word (mem[8..11]) + padw mem_loadw_be.8 + # => [RECIPIENT_WORD] + + # Push note_type, note_tag, deadline + mem_load.6 mem_load.5 mem_load.4 + # => [deadline, note_tag, note_type, RECIPIENT_WORD] + + # Push MIN_ASSET_OUT word (mem[0..3]) + padw mem_loadw_le.0 + # => [MIN_ASSET_OUT_WORD, deadline, note_tag, note_type, RECIPIENT_WORD] + + # Load note asset to memory at ptr=40 + push.40 exec.active_note::get_assets + # => [num_assets, 40, ...] + swap drop + push.EXPECTED_NUM_ASSETS eq assert.err=ERR_INVALID_NUM_ASSETS + + # Load ASSET_IN word + padw mem_loadw_be.40 + # => [ASSET_IN, MIN_ASSET_OUT_WORD, deadline, note_tag, note_type, RECIPIENT_WORD] + + call.c_prod_pool::do_swap + exec.sys::truncate_stack +end diff --git a/src/masm/notes/xyk_withdraw.masm b/src/masm/notes/xyk_withdraw.masm new file mode 100644 index 0000000..67b83da --- /dev/null +++ b/src/masm/notes/xyk_withdraw.masm @@ -0,0 +1,36 @@ +use miden::protocol::active_note +use zoro::lp_local +use miden::core::sys + +const EXPECTED_NUM_INPUTS = 12 +const ERR_INVALID_NUM_INPUTS = "Invalid number of inputs" + +begin + exec.active_note::get_inputs + # => [num_inputs, dest_ptr] + push.EXPECTED_NUM_INPUTS eq assert.err=ERR_INVALID_NUM_INPUTS + + # note_inputs layout (dest_ptr = 0): + # mem[0] = [lp_amount, 0, 0, 0] (word 0) + # mem[4] = [note_tag, note_type, 0, 0] (word 1) + # mem[8] = [r0, r1, r2, r3] (word 2 - recipient digest) + + # Push recipient word (word 2) + padw mem_loadw_be.8 + # => [RECIPIENT_WORD] + + # Push note_type, note_tag + mem_load.5 mem_load.4 + # => [note_tag, note_type, RECIPIENT_WORD] + + # Push user_id (sender) + exec.active_note::get_sender + # => [sender_prefix, sender_suffix, note_tag, note_type, RECIPIENT_WORD] + + # Push LP_AMOUNT as a word [0, 0, 0, lp_amount] + padw mem_loadw_le.0 + # => [LP_AMOUNT_WORD, sender_prefix, sender_suffix, note_tag, note_type, RECIPIENT_WORD] + + call.lp_local::withdraw + exec.sys::truncate_stack +end diff --git a/src/pages/Launchpad.tsx b/src/pages/Launchpad.tsx index 074acac..9ce4a86 100644 --- a/src/pages/Launchpad.tsx +++ b/src/pages/Launchpad.tsx @@ -1,197 +1,370 @@ import { Footer } from '@/components/Footer'; import { Header } from '@/components/Header'; import { Button } from '@/components/ui/button'; -import { Skeleton } from '@/components/ui/skeleton'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; import { UnifiedWalletButton } from '@/components/UnifiedWalletButton'; -import { useClaimNotes } from '@/hooks/useClaimNotes'; +import useLaunchpad, { + getMidenscanAccountUrl, + getMidenscanTxUrl, + type LaunchSuccess, +} from '@/hooks/useLaunchpad'; import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; - -import { ZoroContext } from '@/providers/ZoroContext'; -import { type FaucetMintResult, mintFromFaucet } from '@/services/faucet'; -import { useCallback, useContext, useEffect, useState } from 'react'; +import { truncateId } from '@/lib/format'; +import { ArrowLeft, CheckCircle, ExternalLink, Loader2 } from 'lucide-react'; +import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; +import { parseUnits } from 'viem'; + +const SYMBOL_MIN = 3; +const SYMBOL_MAX = 6; +const DECIMALS_MIN = 0; +const DECIMALS_MAX = 12; -interface MintStatus { - readonly isLoading: boolean; - readonly lastResult: FaucetMintResult | null; - readonly lastAttempt: number; - readonly showMessage: boolean; +function validateSymbol(s: string): string | null { + const trimmed = s.trim().toUpperCase(); + if (trimmed.length < SYMBOL_MIN) { + return `Symbol must be at least ${SYMBOL_MIN} characters`; + } + if (trimmed.length > SYMBOL_MAX) { + return `Symbol must be at most ${SYMBOL_MAX} characters`; + } + if (!/^[A-Z0-9]+$/.test(trimmed)) return 'Symbol must be letters and numbers only'; + return null; } -type TokenMintStatuses = Record; +function validateDecimals(n: number): string | null { + if (!Number.isInteger(n) || n < DECIMALS_MIN) { + return `Decimals must be at least ${DECIMALS_MIN}`; + } + if (n > DECIMALS_MAX) return `Decimals must be at most ${DECIMALS_MAX}`; + return null; +} -function Faucet() { - const { connected, address } = useUnifiedWallet(); - const { refreshPendingNotes } = useClaimNotes(); - const [mintStatuses, setMintStatuses] = useState( - {} as TokenMintStatuses, - ); - const { tokens, tokensLoading, startExpectingNotes } = useContext(ZoroContext); - const updateMintStatus = useCallback(( - tokenSymbol: string, - updates: Partial, - ): void => { - setMintStatuses(prev => ({ - ...prev, - [tokenSymbol]: { - ...prev[tokenSymbol], - ...updates, - }, - })); - }, []); +function validateInitialSupply(raw: string, decimals: number): string | null { + if (!raw.trim()) return 'Initial supply is required'; + let amount: bigint; + try { + amount = parseUnits(raw.trim(), decimals); + } catch { + return 'Invalid amount'; + } + if (amount <= 0n) return 'Initial supply must be greater than 0'; + return null; +} - useEffect(() => { - for (const token of Object.values(tokens)) { - // init token states - if (!mintStatuses[token.faucetIdBech32]) { - // eslint-disable-next-line - updateMintStatus(token.faucetIdBech32, { - isLoading: false, - lastAttempt: 0, - lastResult: null, - showMessage: false, - }); - } - } - }, [tokens, mintStatuses, setMintStatuses, updateMintStatus]); +export default function Launchpad() { + const { connected } = useUnifiedWallet(); + const { launchToken, error, clearError } = useLaunchpad(); + const [symbol, setSymbol] = useState(''); + const [decimals, setDecimals] = useState('6'); + const [initialSupply, setInitialSupply] = useState(''); + const [touched, setTouched] = useState({ + symbol: false, + decimals: false, + supply: false, + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [successResult, setSuccessResult] = useState(null); + const [copiedId, setCopiedId] = useState<'tx' | 'faucet' | null>(null); - const requestTokens = useCallback(async (tokenFaucetId: string): Promise => { - if (!connected || !address) { - return; - } - const token = tokens[tokenFaucetId]; - if (!token || !token.faucetIdBech32) { + const symbolError = touched.symbol ? validateSymbol(symbol) : null; + const decimalsNum = parseInt(decimals, 10); + const decimalsError = touched.decimals + ? validateDecimals(decimalsNum) + : (Number.isNaN(decimalsNum) ? 'Invalid number' : null); + const supplyError = touched.supply + ? validateInitialSupply(initialSupply, Number.isNaN(decimalsNum) ? 18 : decimalsNum) + : null; + + const canSubmit = connected + && !symbolError + && !decimalsError + && !supplyError + && symbol.trim().length >= SYMBOL_MIN + && symbol.trim().length <= SYMBOL_MAX + && !Number.isNaN(decimalsNum) + && decimalsNum >= DECIMALS_MIN + && decimalsNum <= DECIMALS_MAX + && initialSupply.trim() !== '' + && validateInitialSupply(initialSupply, decimalsNum) === null; + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + setTouched({ symbol: true, decimals: true, supply: true }); + if (!canSubmit) return; + const sym = symbol.trim().toUpperCase(); + const dec = decimalsNum; + let supplyBigint: bigint; + try { + supplyBigint = parseUnits(initialSupply.trim(), dec); + } catch { return; } - updateMintStatus(tokenFaucetId, { - isLoading: true, - lastAttempt: Date.now(), - showMessage: false, + if (supplyBigint <= 0n) return; + setIsSubmitting(true); + clearError(); + const result = await launchToken({ + symbol: sym, + decimals: dec, + initialSupply: supplyBigint, }); - startExpectingNotes(); - - try { - const result = await mintFromFaucet( - address.split('_')[0], - token.faucetIdBech32, - ); - updateMintStatus(tokenFaucetId, { - isLoading: false, - lastResult: result, - showMessage: false, - }); - // Refresh pending notes count after successful mint (sync with network) - if (result.success) { - setTimeout(() => refreshPendingNotes(), 2000); - } - setTimeout(() => { - updateMintStatus(tokenFaucetId, { - showMessage: true, - }); - }, 100); - setTimeout(() => { - updateMintStatus(tokenFaucetId, { - showMessage: false, - }); - }, 5100); - } catch (error) { - const errorResult: FaucetMintResult = { - success: false, - message: error instanceof Error ? error.message : 'Unknown error', - }; - updateMintStatus(tokenFaucetId, { - isLoading: false, - lastResult: errorResult, - showMessage: false, - }); - setTimeout(() => { - updateMintStatus(tokenFaucetId, { - showMessage: true, - }); - }, 100); - setTimeout(() => { - updateMintStatus(tokenFaucetId, { - showMessage: false, - }); - }, 5100); + setIsSubmitting(false); + if (result) { + setSuccessResult(result); + setSymbol(''); + setDecimals('6'); + setInitialSupply(''); + setTouched({ symbol: false, decimals: false, supply: false }); + clearError(); } - }, [ - connected, - address, - updateMintStatus, - tokens, - refreshPendingNotes, - startExpectingNotes, - ]); - - const getButtonText = (tokenSymbol: string, status: MintStatus): string => { - return status.isLoading ? `Minting ${tokenSymbol}...` : `Request ${tokenSymbol}`; - }; - - const isButtonDisabled = (status: MintStatus): boolean => { - return status.isLoading || !connected; - }; - - if (tokensLoading) { - return ( -
-
- - -
-
- ); - } + }, [canSubmit, symbol, decimalsNum, initialSupply, launchToken, clearError]); - if (Object.keys(tokens).length === 0) { - return ( -
-
-
-
- -
-
-
- ); - } + useEffect(() => { + if (error) setSuccessResult(null); + }, [error]); + + const copyToClipboard = useCallback((text: string, kind: 'tx' | 'faucet') => { + navigator.clipboard.writeText(text); + setCopiedId(kind); + setTimeout(() => setCopiedId(null), 2000); + }, []); return (
Launchpad - ZoroSwap | DeFi on Miden - - - - - +
-
-
-
-

Token launchpad

-
- - - - -
-
-
- - - -
-
+
+ + + Back to Swap + + + + + + Token Launchpad + +

+ Create a new faucet token and mint initial supply to your wallet. +

+
+ + {successResult + ? ( +
+
+ + Token launched successfully +
+

+ Claim the note with your token supply in your wallet to receive the + tokens. You can launch another token below. +

+
+
+ +
+ + + + +
+ + + View faucet on MidenScan + +
+
+ +
+ + + + +
+ + + View transaction on MidenScan + +
+
+

+ Go to your{' '} + wallet and claim the pending note to receive your token supply in + your wallet. +

+
+
+
+ ) + : null} + + {!connected + ? ( +
+

+ Connect your wallet to launch a token. +

+ +
+ ) + : ( +
+
+ + { + setSymbol(e.target.value.toUpperCase().slice(0, SYMBOL_MAX)); + if (error) clearError(); + }} + onBlur={() => + setTouched((t) => ({ ...t, symbol: true }))} + maxLength={SYMBOL_MAX} + className={symbolError ? 'border-destructive' : ''} + aria-invalid={!!symbolError} + aria-describedby={symbolError + ? 'launchpad-symbol-error' + : undefined} + /> + {symbolError && ( +

+ {symbolError} +

+ )} +

+ {SYMBOL_MIN}–{SYMBOL_MAX} characters, letters and numbers only +

+
+ +
+ + { + setDecimals(e.target.value); + if (error) clearError(); + }} + onBlur={() => setTouched((t) => ({ ...t, decimals: true }))} + className={decimalsError ? 'border-destructive' : ''} + aria-invalid={!!decimalsError} + /> + {decimalsError && ( +

{decimalsError}

+ )} +
+ +
+ + { + setInitialSupply(e.target.value); + if (error) clearError(); + }} + onBlur={() => setTouched((t) => ({ ...t, supply: true }))} + className={supplyError ? 'border-destructive' : ''} + aria-invalid={!!supplyError} + /> + {supplyError && ( +

{supplyError}

+ )} +
+ + {error && ( +
+ {error} +
+ )} + + +
+ )} +
+
); } - -export default Faucet; diff --git a/src/pages/LiquidityPools.tsx b/src/pages/LiquidityPools.tsx index 6c9494f..1f0f333 100644 --- a/src/pages/LiquidityPools.tsx +++ b/src/pages/LiquidityPools.tsx @@ -1,34 +1,31 @@ +import { AllDropdown } from '@/components/AllDropdown'; import { Footer } from '@/components/Footer'; import { Header } from '@/components/Header'; import LiquidityPoolsTable from '@/components/LiquidityPoolsTable'; +import { type LpDetails, OrderStatus, type TxResult } from '@/components/OrderStatus'; +import PoolModal from '@/components/PoolModal'; +import type { LpActionType } from '@/components/PoolModal'; import { PositionCard } from '@/components/PositionCard'; -import { AllDropdown } from '@/components/AllDropdown'; +import { SelectPoolModal } from '@/components/SelectPoolModal'; +import { Button } from '@/components/ui/button'; import { useLPBalances } from '@/hooks/useLPBalances'; import { usePoolsBalances } from '@/hooks/usePoolsBalances'; import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; import { useOrderUpdates } from '@/hooks/useWebSocket'; import { ModalContext } from '@/providers/ModalContext'; import { ZoroContext } from '@/providers/ZoroContext'; -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { OrderStatus, type LpDetails, type TxResult } from '@/components/OrderStatus'; -import PoolModal from '@/components/PoolModal'; -import type { LpActionType } from '@/components/PoolModal'; -import { CreatePoolWizard } from '@/components/CreatePoolWizard'; -import { SelectPoolModal } from '@/components/SelectPoolModal'; -import { Button } from '@/components/ui/button'; function LiquidityPools() { const navigate = useNavigate(); - const { data: poolsInfo, refetch: refetchPoolsInfo, isLoading: isLoadingPools } = usePoolsInfo(); - const { data: poolBalances, refetch: refetchPoolBalances, isLoading: isLoadingBalances } = usePoolsBalances(); + const { data: poolsInfo, refetch: refetchPoolsInfo, isLoading: isLoadingPools } = + usePoolsInfo(); + const { + data: poolBalances, + refetch: refetchPoolBalances, + isLoading: isLoadingBalances, + } = usePoolsBalances(); const modalContext = useContext(ModalContext); const { tokens } = useContext(ZoroContext); const { orderStatus, registerCallback } = useOrderUpdates(); @@ -91,10 +88,6 @@ function LiquidityPools() { [navigate], ); - const openCreatePoolWizard = useCallback(() => { - modalContext.openModal(); - }, [modalContext, refetchPoolsInfo]); - const openNewPositionModal = useCallback(() => { const pools = poolsInfo?.liquidityPools ?? []; modalContext.openModal( @@ -114,7 +107,9 @@ function LiquidityPools() { return liquidityPools .filter((pool) => pool.poolType === 'hfAMM') .map((pool) => { - const balance = poolBalances.find((b) => b.faucetIdBech32 === pool.faucetIdBech32); + const balance = poolBalances.find((b) => + b.faucetIdBech32 === pool.faucetIdBech32 + ); const lp = lpBalances[pool.faucetIdBech32] ?? BigInt(0); if (!balance || lp <= BigInt(0)) return null; return { pool, poolBalance: balance, lpBalance: lp }; @@ -180,7 +175,6 @@ function LiquidityPools() { tokenConfigs={tokenConfigs} openPoolModal={openPoolModal} onPoolRowClick={onPoolRowClick} - onCreatePool={openCreatePoolWizard} isLoading={isLoadingPools || isLoadingBalances} /> @@ -192,9 +186,9 @@ function LiquidityPools() { onClose={() => setIsSuccessModalOpen(false)} swapResult={txResult} lpDetails={lpDetails} - orderStatus={ - txResult?.noteId ? orderStatus[txResult.noteId]?.status : undefined - } + orderStatus={txResult?.noteId + ? orderStatus[txResult.noteId]?.status + : undefined} /> )}
diff --git a/src/pages/NewXykPool.tsx b/src/pages/NewXykPool.tsx new file mode 100644 index 0000000..c8db395 --- /dev/null +++ b/src/pages/NewXykPool.tsx @@ -0,0 +1,16 @@ +import { Footer } from '@/components/Footer'; +import { Header } from '@/components/Header'; +import XykWizard from '@/components/xyk-wizard/XykWizard'; + +export default function NewXykPool() { + return ( +
+ Create pool - ZoroSwap | DeFi on Miden +
+
+ +
+
+
+ ); +} diff --git a/src/pages/PoolDetail.tsx b/src/pages/PoolDetail.tsx index 159d576..11e14e1 100644 --- a/src/pages/PoolDetail.tsx +++ b/src/pages/PoolDetail.tsx @@ -1,37 +1,32 @@ +import AssetIcon from '@/components/AssetIcon'; import { Footer } from '@/components/Footer'; import { Header } from '@/components/Header'; -import { OrderStatus, type LpDetails, type TxResult } from '@/components/OrderStatus'; +import { type LpDetails, OrderStatus, type TxResult } from '@/components/OrderStatus'; import PoolModal from '@/components/PoolModal'; import type { LpActionType } from '@/components/PoolModal'; +import { TradingViewCandlesChart } from '@/components/TradingViewCandlesChart'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useLPBalances } from '@/hooks/useLPBalances'; import { usePoolsBalances } from '@/hooks/usePoolsBalances'; import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; import { useOrderUpdates } from '@/hooks/useWebSocket'; +import { fullNumberBigintFormat, prettyBigintFormat, truncateId } from '@/lib/format'; +import { cn } from '@/lib/utils'; +import { getMockPoolCandles, getMockRecentTransactions } from '@/mocks/poolDetailMocks'; import { ModalContext } from '@/providers/ModalContext'; import { ZoroContext } from '@/providers/ZoroContext'; -import { fullNumberBigintFormat, prettyBigintFormat, truncateId } from '@/utils/format'; -import { AlertTriangle, ExternalLink, ArrowLeft } from 'lucide-react'; -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { AlertTriangle, ArrowLeft, ExternalLink } from 'lucide-react'; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; -import AssetIcon from '@/components/AssetIcon'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { TradingViewCandlesChart } from '@/components/TradingViewCandlesChart'; -import { cn } from '@/lib/utils'; -import { getMockPoolCandles, getMockRecentTransactions } from '@/mocks/poolDetailMocks'; const feeTierForSymbol = (symbol: string) => /USDC|USDT|DAI|BUSD/i.test(symbol) ? '0.01%' : '0.30%'; /** Saturation = (reserve / total_liabilities) as percentage (can exceed 100). hfAMM only. */ -function getSaturationPercent(poolBalance: { reserve: bigint; totalLiabilities: bigint }): number | null { +function getSaturationPercent( + poolBalance: { reserve: bigint; totalLiabilities: bigint }, +): number | null { const { reserve, totalLiabilities } = poolBalance; if (totalLiabilities === BigInt(0)) return null; return (Number(reserve) / Number(totalLiabilities)) * 100; @@ -39,7 +34,9 @@ function getSaturationPercent(poolBalance: { reserve: bigint; totalLiabilities: function getSaturationColorClass(pct: number): string { if (pct < 15 || pct > 185) return 'text-red-600 border-red-600/30 bg-red-500/10'; - if ((pct >= 15 && pct < 30) || (pct >= 170 && pct <= 185)) return 'text-yellow-600 border-yellow-600/30 bg-yellow-500/10'; + if ((pct >= 15 && pct < 30) || (pct >= 170 && pct <= 185)) { + return 'text-yellow-600 border-yellow-600/30 bg-yellow-500/10'; + } if (pct >= 30 && pct < 170) return 'text-green-600 border-green-600/30 bg-green-500/10'; return 'text-muted-foreground border-border bg-muted/30'; } @@ -157,7 +154,9 @@ export default function PoolDetail() { }); const isHfAmm = pool.poolType === 'hfAMM'; const saturationPercent = isHfAmm ? getSaturationPercent(poolBalance) : null; - const saturationColor = saturationPercent != null ? getSaturationColorClass(saturationPercent) : ''; + const saturationColor = saturationPercent != null + ? getSaturationColorClass(saturationPercent) + : ''; const pairLabel = isHfAmm ? `${pool.symbol}` : `${pool.symbol} / USDC`; return ( @@ -286,8 +285,10 @@ export default function PoolDetail() {
- - Pool Composition + + + Pool Composition + {isHfAmm @@ -316,7 +317,12 @@ export default function PoolDetail() { {pool.symbol}
-

{prettyBigintFormat({ value: poolBalance.reserve, expo: decimals })}

+

+ {prettyBigintFormat({ + value: poolBalance.reserve, + expo: decimals, + })} +

@@ -326,14 +332,23 @@ export default function PoolDetail() { USDC
-

{prettyBigintFormat({ value: poolBalance.totalLiabilities, expo: decimals })}

+

+ {prettyBigintFormat({ + value: poolBalance.totalLiabilities, + expo: decimals, + })} +

{(() => { const total = poolBalance.reserve + poolBalance.totalLiabilities; - const reservePct = total > 0n ? Number((poolBalance.reserve * 100n) / total) : 50; - const liabPct = total > 0n ? Number((poolBalance.totalLiabilities * 100n) / total) : 50; + const reservePct = total > 0n + ? Number((poolBalance.reserve * 100n) / total) + : 50; + const liabPct = total > 0n + ? Number((poolBalance.totalLiabilities * 100n) / total) + : 50; return ( <>
@@ -347,7 +362,9 @@ export default function PoolDetail() { />

- {total > 0n ? `${pool.symbol} ${reservePct}% · USDC ${liabPct}%` : '—'} + {total > 0n + ? `${pool.symbol} ${reservePct}% · USDC ${liabPct}%` + : '—'}

); @@ -382,11 +399,21 @@ export default function PoolDetail() { <>
Total Liabilities - {fullNumberBigintFormat({ value: poolBalance.totalLiabilities, expo: decimals })} + + {fullNumberBigintFormat({ + value: poolBalance.totalLiabilities, + expo: decimals, + })} +
Reserve - {fullNumberBigintFormat({ value: poolBalance.reserve, expo: decimals })} + + {fullNumberBigintFormat({ + value: poolBalance.reserve, + expo: decimals, + })} +
)} @@ -403,9 +430,8 @@ export default function PoolDetail() {

- This pool's tokens may have price correlation. Impermanent - loss is possible when prices move. Consider concentrated ranges - carefully. + This pool's tokens may have price correlation. Impermanent loss + is possible when prices move. Consider concentrated ranges carefully.

@@ -416,7 +442,9 @@ export default function PoolDetail() { {!isHfAmm && ( - Recent Transactions + + Recent Transactions +
@@ -433,13 +461,21 @@ export default function PoolDetail() { {mockRecentTxs.map((tx, i) => ( - + {tx.type} {tx.amountIn} {tx.amountOut} - {tx.account} - {tx.timeAgo} + + {tx.account} + + + {tx.timeAgo} + ))} @@ -486,12 +522,11 @@ export default function PoolDetail() { onClose={() => setIsSuccessModalOpen(false)} swapResult={txResult} lpDetails={lpDetails} - orderStatus={ - txResult?.noteId ? orderStatus[txResult.noteId]?.status : undefined - } + orderStatus={txResult?.noteId + ? orderStatus[txResult.noteId]?.status + : undefined} /> )}
); } - diff --git a/src/pages/Pools.tsx b/src/pages/Pools.tsx index 4f41568..b3c91b4 100644 --- a/src/pages/Pools.tsx +++ b/src/pages/Pools.tsx @@ -1,16 +1,15 @@ -import { CreatePoolWizard, readCreatedPools, clearCreatedPools } from '@/components/CreatePoolWizard'; +import { AllDropdown } from '@/components/AllDropdown'; import { Footer } from '@/components/Footer'; import { Header } from '@/components/Header'; -import { AllDropdown } from '@/components/AllDropdown'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { ModalContext } from '@/providers/ModalContext'; -import { useCallback, useContext, useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { clearCreatedPools, readCreatedPools } from '@/lib/poolUtils'; +import { emptyFn } from '@/lib/shared'; import { Trash2 } from 'lucide-react'; +import { useCallback, useEffect, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; export default function Pools() { - const modalContext = useContext(ModalContext); const navigate = useNavigate(); const [createdPools, setCreatedPools] = useState(() => readCreatedPools()); const refreshCreated = useCallback(() => setCreatedPools(readCreatedPools()), []); @@ -23,12 +22,12 @@ export default function Pools() { return () => window.removeEventListener('storage', onStorage); }, [refreshCreated]); - const openCreateWizard = useCallback(() => { - modalContext.openModal(); - }, [modalContext, refreshCreated]); - const handleDeleteDrafts = useCallback(() => { - if (window.confirm('Are you sure you want to delete all draft pools? This cannot be undone.')) { + if ( + window.confirm( + 'Are you sure you want to delete all draft pools? This cannot be undone.', + ) + ) { clearCreatedPools(); refreshCreated(); } @@ -58,13 +57,15 @@ export default function Pools() { Delete drafts )} - + + +
@@ -77,7 +78,8 @@ export default function Pools() { No pools yet

- Start earning by providing liquidity to pools. Create a pool or browse existing pools to add liquidity. + Start earning by providing liquidity to pools. Create a pool or browse + existing pools to add liquidity.

- + + +
diff --git a/src/providers/ModalContext.tsx b/src/providers/ModalContext.tsx index 7ee77e2..2d57492 100644 --- a/src/providers/ModalContext.tsx +++ b/src/providers/ModalContext.tsx @@ -1,5 +1,5 @@ +import { emptyFn } from '@/lib/shared'; import { createContext, type ReactNode } from 'react'; -import { emptyFn } from '../utils/shared'; interface ModalContextProps { openModal: (c: ReactNode) => void; diff --git a/src/providers/OracleContext.ts b/src/providers/OracleContext.ts index 0fcfb05..f43f689 100644 --- a/src/providers/OracleContext.ts +++ b/src/providers/OracleContext.ts @@ -1,4 +1,4 @@ -import { emptyFn } from '@/utils/shared'; +import { emptyFn } from '@/lib/shared'; import { createContext, useContext, useMemo } from 'react'; export interface PriceData { diff --git a/src/providers/ZoroContext.ts b/src/providers/ZoroContext.ts index 8fdf231..75367f3 100644 --- a/src/providers/ZoroContext.ts +++ b/src/providers/ZoroContext.ts @@ -10,6 +10,8 @@ import type { import { createContext } from 'react'; import type { FaucetParams, TokenConfig } from './ZoroProvider'; +export type TokenConfigWithBalance = { config: TokenConfig; amount: bigint }; + type ZoroProviderState = { poolAccountId?: AccountId; client?: WebClient; @@ -36,6 +38,7 @@ type ZoroProviderState = { accountId: AccountId, amount: bigint, ) => Promise; + getAvailableTokens: () => Promise; }; const initialState: ZoroProviderState = { @@ -54,6 +57,7 @@ const initialState: ZoroProviderState = { refreshPendingNotes: () => Promise.resolve(), createFaucet: () => Promise.resolve(undefined), mintFromFaucet: () => Promise.resolve(''), + getAvailableTokens: () => Promise.resolve([]), }; export const ZoroContext = createContext(initialState); diff --git a/src/providers/ZoroProvider.tsx b/src/providers/ZoroProvider.tsx index dbb33ae..dd58c37 100644 --- a/src/providers/ZoroProvider.tsx +++ b/src/providers/ZoroProvider.tsx @@ -2,18 +2,20 @@ import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; import { clientMutex } from '@/lib/clientMutex'; import { NETWORK } from '@/lib/config'; -import { bech32ToAccountId, instantiateClient } from '@/lib/utils'; +import { accountIdToBech32, bech32ToAccountId, instantiateClient } from '@/lib/utils'; import { AccountId, AccountStorageMode, Address, AuthScheme, + BasicFungibleFaucetComponent, Endpoint, Note, NoteType, RpcClient, WebClient, } from '@miden-sdk/miden-sdk'; +import { useAssetMetadata } from '@miden-sdk/react'; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParaClient } from './ParaClientContext'; import { ZoroContext } from './ZoroContext'; @@ -183,6 +185,7 @@ export function ZoroProvider({ throw new Error('Client not initialized'); } return withClientLock(async () => { + console.log('Creating a new faucet.'); const { symbol, decimals, initialSupply } = params; const faucet = await client.newFaucet( AccountStorageMode.public(), @@ -192,6 +195,7 @@ export function ZoroProvider({ initialSupply, AuthScheme.AuthRpoFalcon512, ); + console.log('Created faucet ', faucet.bech32id()); return faucet; }); }, [client, withClientLock]); @@ -203,6 +207,11 @@ export function ZoroProvider({ throw new Error('Client not initialized'); } return withClientLock(async () => { + console.log( + `Minting ${amount} from faucet ${accountIdToBech32(faucet_id)} to account ${ + accountIdToBech32(account_id) + }.`, + ); const mintTxRequest = client.newMintTransactionRequest( account_id, faucet_id, @@ -220,6 +229,38 @@ export function ZoroProvider({ [client, withClientLock], ); + const getAvailableTokens = useCallback(async () => { + if (!client || !accountId) { + return []; + } + return withClientLock(async () => { + const acc = await client.getAccount(accountId); + const tokens: { config: TokenConfig; amount: bigint }[] = []; + for (const t of acc?.vault().fungibleAssets() ?? []) { + const f = t.faucetId(); + const amount = t.amount(); + const faucet = await rpcClient.getAccountDetails(f); + const account = faucet.account(); + if (account) { + const faucet = BasicFungibleFaucetComponent.fromAccount(account); + const symbol = faucet.symbol().toString(); + tokens.push({ + config: { + symbol, + decimals: faucet.decimals(), + name: symbol, + faucetId: f, + faucetIdBech32: accountIdToBech32(f), + oracleId: '0x', + } as TokenConfig, + amount, + }); + } + } + return tokens; + }); + }, [client, withClientLock, accountId, rpcClient]); + // Periodic refresh for Para wallet users useEffect(() => { if (walletType !== 'para' || !accountId) { @@ -257,6 +298,7 @@ export function ZoroProvider({ refreshPendingNotes, createFaucet, mintFromFaucet, + getAvailableTokens, }; }, [ accountId, @@ -275,6 +317,7 @@ export function ZoroProvider({ refreshPendingNotes, createFaucet, mintFromFaucet, + getAvailableTokens, ]); return ( From 09926994d4d6846059f0bfcf8b719d8f341921d7 Mon Sep 17 00:00:00 2001 From: mico Date: Sun, 8 Mar 2026 03:54:49 +0100 Subject: [PATCH 09/49] Spawning pools in wizard --- src/components/xyk-wizard/XykWizard.tsx | 61 +++++++++++++++++++++++-- src/hooks/useNewXykPool.ts | 4 -- src/lib/DeployXykPool.ts | 28 +++++------- src/lib/XykDepositNote.ts | 9 ++-- src/providers/ZoroProvider.tsx | 1 - 5 files changed, 71 insertions(+), 32 deletions(-) delete mode 100644 src/hooks/useNewXykPool.ts diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index cdc2260..d918f27 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -3,17 +3,20 @@ import { Card, CardContent } from '@/components/ui/card'; import { UnifiedWalletButton } from '@/components/UnifiedWalletButton'; import useTokensWithBalance from '@/hooks/useTokensWithBalance'; import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; +import { deployNewPool } from '@/lib/DeployXykPool'; import { type CreatedPoolDraft, readCreatedPools, writeCreatedPools, } from '@/lib/poolUtils'; import { accountIdToBech32 } from '@/lib/utils'; -import { type TokenConfigWithBalance } from '@/providers/ZoroContext'; -import type { TokenConfig } from '@/providers/ZoroProvider'; +import { compileXykDepositTransaction } from '@/lib/XykDepositNote'; +import { type TokenConfigWithBalance, ZoroContext } from '@/providers/ZoroContext'; +import { type TokenConfig, ZoroProvider } from '@/providers/ZoroProvider'; +import { TransactionType } from '@demox-labs/miden-wallet-adapter'; import { AccountId } from '@miden-sdk/miden-sdk'; import { ChevronLeft } from 'lucide-react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import XykStep1 from './steps/XykWizardStep1'; import XykStep2 from './steps/XykWizardStep2'; @@ -155,7 +158,8 @@ export interface XykStepProps { } const XykWizard = () => { - const { connected } = useUnifiedWallet(); + const { connected, requestTransaction } = useUnifiedWallet(); + const { client, accountId } = useContext(ZoroContext); const [form, setForm] = useState(() => readPersistedWizard().form); const [step, setStep] = useState(() => readPersistedWizard().step); const tokensWithBalance = useTokensWithBalance(); @@ -195,6 +199,45 @@ const XykWizard = () => { } }, [canGoBackInWizard, step]); + const launchXykPool = useCallback( + async ( + { token0, token1, amount0, amount1 }: { + token0: AccountId; + token1: AccountId; + amount0: bigint; + amount1: bigint; + }, + ) => { + try { + if (!client) { + throw new Error('Client not initialized'); + } + if (!accountId) { + throw new Error('User not logged in'); + } + + const { newPoolId } = await deployNewPool({ client, token0, token1 }); + const { tx } = await compileXykDepositTransaction({ + token0, + token1, + amount0, + amount1, + userAccountId: accountId, + poolAccountId: newPoolId, + client, + }); + const txId = await requestTransaction({ + type: TransactionType.Custom, + payload: tx, + }); + await client.syncState(); + } catch (e) { + console.error(e); + } + }, + [client, requestTransaction, accountId], + ); + const onCreate = useCallback(async () => { if ( form.amountA == null || form.amountB == null || form.tokenA == null @@ -224,8 +267,16 @@ const XykWizard = () => { }; const existing = readCreatedPools(); writeCreatedPools([draft, ...existing]); + + await launchXykPool({ + token0: form.tokenA, + token1: form.tokenB, + amount0: form.amountA, + amount1: form.amountB, + }); + next(); - }, [form, tokensWithBalance, next]); + }, [form, tokensWithBalance, next, launchXykPool]); const stepTitle = step === 0 ? 'Create a new Liquidity Pool' diff --git a/src/hooks/useNewXykPool.ts b/src/hooks/useNewXykPool.ts deleted file mode 100644 index 565f57f..0000000 --- a/src/hooks/useNewXykPool.ts +++ /dev/null @@ -1,4 +0,0 @@ -const useSpawnNewPool = () => { -}; - -export default useSpawnNewPool; diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 46286c3..01af78d 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -2,7 +2,6 @@ import { AccountId, AccountType, Felt, - NoteType, StorageSlot, WebClient, Word, @@ -13,7 +12,6 @@ import lp_local from '@/masm/accounts/lp_local.masm?raw'; import math from '@/masm/accounts/math.masm?raw'; import storage_utils from '@/masm/accounts/storage_utils.masm?raw'; -import type { TokenConfig } from '@/providers/ZoroProvider'; import { StorageMap } from '@miden-sdk/miden-sdk'; import { AccountComponent } from '@miden-sdk/miden-sdk'; import { AuthSecretKey } from '@miden-sdk/miden-sdk'; @@ -21,13 +19,9 @@ import { AccountBuilder } from '@miden-sdk/miden-sdk'; import { AccountStorageMode } from '@miden-sdk/miden-sdk'; export interface DeployNewPoolParams { - token0: TokenConfig; - token1: TokenConfig; - amount0: bigint; - amount1: bigint; - userAccountId: AccountId; + token0: AccountId; + token1: AccountId; client: WebClient; - noteType: NoteType; } export interface DeployResult { @@ -39,10 +33,10 @@ export interface DeployResult { const build_lp_local_component = (client: WebClient) => { const builder = client.createCodeBuilder(); const math_lib = builder.buildLibrary('zoro::math', math); - // const storage_utils_lib = builder.buildLibrary('zoro::storage_utils', storage_utils); + const storage_utils_lib = builder.buildLibrary('zoro::storage_utils', storage_utils); builder.linkStaticLibrary(math_lib); - // builder.linkStaticLibrary(storage_utils_lib) - const c_prod_component = builder.buildLibrary('zoro::c_prod', c_prod); + builder.linkStaticLibrary(storage_utils_lib); + const c_prod_component = builder.buildLibrary('zoro::lp_local', c_prod); return c_prod_component; }; const build_c_prod_component = (client: WebClient) => { @@ -51,7 +45,7 @@ const build_c_prod_component = (client: WebClient) => { const storage_utils_lib = builder.buildLibrary('zoro::storage_utils', storage_utils); builder.linkStaticLibrary(math_lib); builder.linkStaticLibrary(storage_utils_lib); - const lp_local_component = builder.buildLibrary('zoro::c_prod', lp_local); + const lp_local_component = builder.buildLibrary('zoro::c_prod_pool', lp_local); return lp_local_component; }; @@ -72,10 +66,10 @@ export async function deployNewPool({ new Felt(BigInt(0)), ]), Word.newFromFelts([ - token1.faucetId.suffix(), - token1.faucetId.prefix(), - token0.faucetId.suffix(), - token0.faucetId.prefix(), + token1.suffix(), + token1.prefix(), + token0.suffix(), + token0.prefix(), ]), ); @@ -136,6 +130,6 @@ export async function deployNewPool({ await client.syncState(); return { - accountId: contract.account.id(), + newPoolId: contract.account.id(), }; } diff --git a/src/lib/XykDepositNote.ts b/src/lib/XykDepositNote.ts index 68a858e..3dcb6a6 100644 --- a/src/lib/XykDepositNote.ts +++ b/src/lib/XykDepositNote.ts @@ -18,12 +18,11 @@ import { import zoropool from '@/masm/accounts/zoropool.masm?raw'; import DEPOSIT_SCRIPT from '@/masm/notes/xyk_deposit.masm?raw'; -import type { TokenConfig } from '@/providers/ZoroProvider'; import { accountIdToBech32, generateRandomSerialNumber } from './utils'; export interface DepositParams { - token0: TokenConfig; - token1: TokenConfig; + token0: AccountId; + token1: AccountId; amount0: bigint; amount1: bigint; userAccountId: AccountId; @@ -68,8 +67,8 @@ export async function compileXykDepositTransaction({ ]), ); - const asset0 = new FungibleAsset(token0.faucetId, amount0); - const asset1 = new FungibleAsset(token1.faucetId, amount1); + const asset0 = new FungibleAsset(token0, amount0); + const asset1 = new FungibleAsset(token1, amount1); const noteAssets = new NoteAssets([asset0, asset1]); const note = new Note( diff --git a/src/providers/ZoroProvider.tsx b/src/providers/ZoroProvider.tsx index dd58c37..2844ecf 100644 --- a/src/providers/ZoroProvider.tsx +++ b/src/providers/ZoroProvider.tsx @@ -15,7 +15,6 @@ import { RpcClient, WebClient, } from '@miden-sdk/miden-sdk'; -import { useAssetMetadata } from '@miden-sdk/react'; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParaClient } from './ParaClientContext'; import { ZoroContext } from './ZoroContext'; From c79886d861e0e202525a9d68cd538885eafb601c Mon Sep 17 00:00:00 2001 From: mico Date: Mon, 9 Mar 2026 09:00:42 +0100 Subject: [PATCH 10/49] Pool deployment & deposit working --- src/lib/DeployXykPool.ts | 65 ++++++++++++++++++---------- src/lib/XykDepositNote.ts | 5 ++- src/lib/utils.ts | 4 +- src/masm/accounts/lp_local.masm | 4 ++ src/masm/accounts/math.masm | 1 - src/masm/accounts/storage_utils.masm | 1 - src/pages/Launchpad.tsx | 8 +++- 7 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 01af78d..fd8db12 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -30,23 +30,38 @@ export interface DeployResult { newPool: AccountId; } -const build_lp_local_component = (client: WebClient) => { +export const build_math_lib = (client: WebClient) => { + const builder = client.createCodeBuilder(); + return builder.buildLibrary('zoro::math', math); +}; +export const build_storage_utils = (client: WebClient) => { + const math_lib = build_math_lib(client); const builder = client.createCodeBuilder(); - const math_lib = builder.buildLibrary('zoro::math', math); - const storage_utils_lib = builder.buildLibrary('zoro::storage_utils', storage_utils); builder.linkStaticLibrary(math_lib); - builder.linkStaticLibrary(storage_utils_lib); - const c_prod_component = builder.buildLibrary('zoro::lp_local', c_prod); - return c_prod_component; + return builder.buildLibrary('zoro::storage_utils', storage_utils); }; -const build_c_prod_component = (client: WebClient) => { + +export const build_lp_local_lib = (client: WebClient) => { + const math_lib = build_math_lib(client); + const storage_utils = build_storage_utils(client); const builder = client.createCodeBuilder(); - const math_lib = builder.buildLibrary('zoro::math', math); - const storage_utils_lib = builder.buildLibrary('zoro::storage_utils', storage_utils); builder.linkStaticLibrary(math_lib); - builder.linkStaticLibrary(storage_utils_lib); - const lp_local_component = builder.buildLibrary('zoro::c_prod_pool', lp_local); - return lp_local_component; + builder.linkStaticLibrary(storage_utils); + return builder.buildLibrary('zoro::lp_local', lp_local); +}; +const build_c_prod_lib = (client: WebClient) => { + console.log('math'); + const math_lib = build_math_lib(client); + console.log('storage_utils'); + // const storage_utils = build_storage_utils(client); + console.log('lp_local'); + const lp_local = build_lp_local_lib(client); + const builder = client.createCodeBuilder(); + builder.linkStaticLibrary(math_lib); + // builder.linkStaticLibrary(storage_utils); + builder.linkStaticLibrary(lp_local); + console.log('c_prod'); + return builder.buildLibrary('zoro::c_prod_pool', c_prod); }; export async function deployNewPool({ @@ -54,9 +69,6 @@ export async function deployNewPool({ token0, token1, }: DeployNewPoolParams) { - const lp_local_lib = build_lp_local_component(client); - const c_prod_lib = build_c_prod_component(client); - const assets = new StorageMap(); assets.insert( Word.newFromFelts([ @@ -74,7 +86,6 @@ export async function deployNewPool({ ); const assets_mapping_slot = StorageSlot.map('zoro::lp_local::assets_mapping', assets); - const reserve_slot = StorageSlot.emptyValue('zoro::lp_local::reserve'); const total_supply_slot = StorageSlot.emptyValue('zoro::lp_local::total_supply'); const user_deposits_mapping = new StorageMap(); @@ -97,6 +108,16 @@ export async function deployNewPool({ user_deposits_mapping, ); + console.log('lp local build'); + const lp_local_lib = build_lp_local_lib(client); + // console.log('c prod build'); + // const c_prod_lib = build_c_prod_lib(client); + + // const c_prod_pool_component = AccountComponent.fromLibrary(c_prod_lib, [ + // reserve_slot, + // assets_mapping_slot, + // ]); + const lp_local_component = AccountComponent.fromLibrary( lp_local_lib, [ @@ -105,22 +126,22 @@ export async function deployNewPool({ total_supply_slot, user_deposits_slot, ], - ); + ).withSupportsAllTypes(); - const c_prod_pool_component = AccountComponent.fromLibrary(c_prod_lib, []); + console.log('account build'); const walletSeed = new Uint8Array(32); crypto.getRandomValues(walletSeed); - const secretKey = AuthSecretKey.rpoFalconWithRNG(walletSeed); const authComponent = AccountComponent.createAuthComponentFromSecretKey(secretKey); const contract = new AccountBuilder(walletSeed) .accountType(AccountType.RegularAccountUpdatableCode) - .storageMode(AccountStorageMode.public()) - .withAuthComponent(authComponent) + .storageMode(AccountStorageMode.network()) .withComponent(lp_local_component) - .withComponent(c_prod_pool_component) + .withAuthComponent(authComponent) + // .withComponent(c_prod_pool_component) + .withBasicWalletComponent() .build(); await client.addAccountSecretKeyToWebStore( diff --git a/src/lib/XykDepositNote.ts b/src/lib/XykDepositNote.ts index 3dcb6a6..ef5b515 100644 --- a/src/lib/XykDepositNote.ts +++ b/src/lib/XykDepositNote.ts @@ -18,6 +18,7 @@ import { import zoropool from '@/masm/accounts/zoropool.masm?raw'; import DEPOSIT_SCRIPT from '@/masm/notes/xyk_deposit.masm?raw'; +import { build_lp_local_lib } from './DeployXykPool'; import { accountIdToBech32, generateRandomSerialNumber } from './utils'; export interface DepositParams { @@ -44,9 +45,9 @@ export async function compileXykDepositTransaction({ amount1, client, }: DepositParams) { + const lp_local_lib = build_lp_local_lib(client); const builder = client.createCodeBuilder(); - const pool_script = builder.buildLibrary('zoroswap::zoropool', zoropool); - builder.linkDynamicLibrary(pool_script); + builder.linkStaticLibrary(lp_local_lib); const script = builder.compileNoteScript( DEPOSIT_SCRIPT, ); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 07503ea..b4effb3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -46,7 +46,9 @@ export const safeAccountImport = async (client: WebClient, accountId: AccountId) export const accountIdToBech32 = ( accountId: AccountId, ) => { - return accountId.toBech32(createNetworkId(), AccountInterface.BasicWallet).split('_')[0]; + return accountId.toBech32(createNetworkId(), AccountInterface.BasicWallet).split( + '_', + )[0]; }; export const bech32ToAccountId = (bech32str?: string) => { diff --git a/src/masm/accounts/lp_local.masm b/src/masm/accounts/lp_local.masm index abd91bf..4f7bdcd 100644 --- a/src/masm/accounts/lp_local.masm +++ b/src/masm/accounts/lp_local.masm @@ -256,8 +256,12 @@ proc sub_from_reserves#(amount_0: felt, amount_1: felt) end pub proc add_to_reserve_by_asset_id#(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) + drop drop drop + exec.sys::truncate_stack end proc sub_from_reserve_by_asset_id#(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) + drop drop drop + exec.sys::truncate_stack end @locals(3) diff --git a/src/masm/accounts/math.masm b/src/masm/accounts/math.masm index 1c0723c..0d0b55a 100644 --- a/src/masm/accounts/math.masm +++ b/src/masm/accounts/math.masm @@ -174,4 +174,3 @@ end # # => [lp_amount_variant_1_high, lp_amount_variant_1_low, lp_amount_variant_0_high, lp_amount_variant_0_low] # end #end - diff --git a/src/masm/accounts/storage_utils.masm b/src/masm/accounts/storage_utils.masm index aa9344d..b7c9540 100644 --- a/src/masm/accounts/storage_utils.masm +++ b/src/masm/accounts/storage_utils.masm @@ -131,4 +131,3 @@ pub proc set_map_item #(slot_id_prefix: felt, slot_id_suffix: felt, key: Word, v dropw loc_load.8 end - diff --git a/src/pages/Launchpad.tsx b/src/pages/Launchpad.tsx index 9ce4a86..269bb3b 100644 --- a/src/pages/Launchpad.tsx +++ b/src/pages/Launchpad.tsx @@ -19,7 +19,8 @@ import { parseUnits } from 'viem'; const SYMBOL_MIN = 3; const SYMBOL_MAX = 6; const DECIMALS_MIN = 0; -const DECIMALS_MAX = 12; +const DECIMALS_MAX = 4; +const TOTAL_SUPPLY_MAX = 1_000_000; function validateSymbol(s: string): string | null { const trimmed = s.trim().toUpperCase(); @@ -50,6 +51,9 @@ function validateInitialSupply(raw: string, decimals: number): string | null { return 'Invalid amount'; } if (amount <= 0n) return 'Initial supply must be greater than 0'; + if (amount > TOTAL_SUPPLY_MAX) { + return `Initial supply should be lower than ${TOTAL_SUPPLY_MAX}`; + } return null; } @@ -262,7 +266,7 @@ export default function Launchpad() { { setSymbol(e.target.value.toUpperCase().slice(0, SYMBOL_MAX)); From 03c0c3dbb5b1e92b3400faba1a2e86a47ebd7d18 Mon Sep 17 00:00:00 2001 From: mico Date: Mon, 9 Mar 2026 10:57:28 +0100 Subject: [PATCH 11/49] Pool details link on wizard --- src/components/XykPairIcon.tsx | 26 ++++ src/components/xyk-wizard/XykWizard.tsx | 121 ++++++++++++------ .../xyk-wizard/steps/XykWizardStep4.tsx | 59 +++++++-- src/lib/DeployXykPool.ts | 3 + src/lib/XykDepositNote.ts | 2 + src/lib/poolUtils.ts | 19 ++- src/pages/Pools.tsx | 84 +++++++++--- src/providers/UnifiedWalletProvider.tsx | 7 +- 8 files changed, 247 insertions(+), 74 deletions(-) create mode 100644 src/components/XykPairIcon.tsx diff --git a/src/components/XykPairIcon.tsx b/src/components/XykPairIcon.tsx new file mode 100644 index 0000000..e3f9c1b --- /dev/null +++ b/src/components/XykPairIcon.tsx @@ -0,0 +1,26 @@ +export interface XykPairIconProps { + symbolA: string; + symbolB: string; + size?: number; +} + +export function XykPairIcon({ symbolA, symbolB, size = 24 }: XykPairIconProps) { + const letterA = (symbolA || '?')[0].toUpperCase(); + const letterB = (symbolB || '?')[0].toUpperCase(); + return ( + + + {letterA} + + + {letterB} + + + ); +} diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index d918f27..d121850 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -7,15 +7,16 @@ import { deployNewPool } from '@/lib/DeployXykPool'; import { type CreatedPoolDraft, readCreatedPools, + updateCreatedPool, writeCreatedPools, } from '@/lib/poolUtils'; import { accountIdToBech32 } from '@/lib/utils'; import { compileXykDepositTransaction } from '@/lib/XykDepositNote'; import { type TokenConfigWithBalance, ZoroContext } from '@/providers/ZoroContext'; -import { type TokenConfig, ZoroProvider } from '@/providers/ZoroProvider'; +import { type TokenConfig } from '@/providers/ZoroProvider'; import { TransactionType } from '@demox-labs/miden-wallet-adapter'; import { AccountId } from '@miden-sdk/miden-sdk'; -import { ChevronLeft } from 'lucide-react'; +import { AlertCircle, ChevronLeft, Loader2 } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import XykStep1 from './steps/XykWizardStep1'; @@ -117,28 +118,7 @@ function clearPersistedWizard() { const wizardSteps = [XykStep1, XykStep2, XykStep3, XykStep4]; -export const XykPairIcon = ( - { symbolA, symbolB, size = 24 }: { symbolA: string; symbolB: string; size?: number }, -) => { - const letterA = (symbolA || '?')[0].toUpperCase(); - const letterB = (symbolB || '?')[0].toUpperCase(); - return ( - - - {letterA} - - - {letterB} - - - ); -}; +export { XykPairIcon } from '@/components/XykPairIcon'; export interface XykWizardForm { tokenA?: AccountId; @@ -155,6 +135,8 @@ export interface XykStepProps { form: XykWizardForm; setForm: (newForm: XykWizardForm) => void; restart: () => void; + /** Set after successful deploy; used by step 4 for "View pool" link. */ + lastDeployedPoolIdBech32?: string; } const XykWizard = () => { @@ -162,12 +144,26 @@ const XykWizard = () => { const { client, accountId } = useContext(ZoroContext); const [form, setForm] = useState(() => readPersistedWizard().form); const [step, setStep] = useState(() => readPersistedWizard().step); + const [lastDeployedPoolIdBech32, setLastDeployedPoolIdBech32] = useState< + string | undefined + >(undefined); const tokensWithBalance = useTokensWithBalance(); useEffect(() => { writePersistedWizard(step, form); }, [step, form]); + useEffect(() => { + if (step !== 2) setCreateError(null); + }, [step]); + + // Never show step 4 (success) unless we have a deployed pool id (e.g. after refresh we might have step 3 but no pool id). + useEffect(() => { + if (step === 3 && !lastDeployedPoolIdBech32) { + setStep(2); + } + }, [step, lastDeployedPoolIdBech32]); + const canContinueWizard = useMemo(() => { switch (step) { case 0: @@ -199,6 +195,9 @@ const XykWizard = () => { } }, [canGoBackInWizard, step]); + const [isCreating, setIsCreating] = useState(false); + const [createError, setCreateError] = useState(null); + const launchXykPool = useCallback( async ( { token0, token1, amount0, amount1 }: { @@ -207,7 +206,7 @@ const XykWizard = () => { amount0: bigint; amount1: bigint; }, - ) => { + ): Promise => { try { if (!client) { throw new Error('Client not initialized'); @@ -231,8 +230,11 @@ const XykWizard = () => { payload: tx, }); await client.syncState(); + console.log('Deposited, tx of deposit: ', txId); + return newPoolId; } catch (e) { console.error(e); + throw e; } }, [client, requestTransaction, accountId], @@ -268,15 +270,36 @@ const XykWizard = () => { const existing = readCreatedPools(); writeCreatedPools([draft, ...existing]); - await launchXykPool({ - token0: form.tokenA, - token1: form.tokenB, - amount0: form.amountA, - amount1: form.amountB, - }); + setCreateError(null); + setIsCreating(true); + try { + const newPoolId = await launchXykPool({ + token0: form.tokenA, + token1: form.tokenB, + amount0: form.amountA, + amount1: form.amountB, + }); - next(); - }, [form, tokensWithBalance, next, launchXykPool]); + if (newPoolId == null) { + setCreateError('Pool creation failed. Please try again.'); + return; + } + const poolIdBech32 = accountIdToBech32(newPoolId); + setLastDeployedPoolIdBech32(poolIdBech32); + updateCreatedPool(draft.id, { + status: 'deployed', + poolIdBech32, + }); + setStep(3); + } catch (err) { + const message = err instanceof Error + ? err.message + : 'Pool creation failed. Please try again.'; + setCreateError(message); + } finally { + setIsCreating(false); + } + }, [form, tokensWithBalance, launchXykPool]); const stepTitle = step === 0 ? 'Create a new Liquidity Pool' @@ -302,9 +325,10 @@ const XykWizard = () => { tokenMetadata={tokensWithBalance.metadata} loading={tokensWithBalance.loading} restart={restart} + lastDeployedPoolIdBech32={lastDeployedPoolIdBech32} /> ); - }, [step, form, tokensWithBalance, restart]); + }, [step, form, tokensWithBalance, restart, lastDeployedPoolIdBech32]); if (!connected) { return ( @@ -405,13 +429,28 @@ const XykWizard = () => { )} {step === 2 && ( - + <> + + {createError && ( +
+ + {createError} +
+ )} + )}
diff --git a/src/components/xyk-wizard/steps/XykWizardStep4.tsx b/src/components/xyk-wizard/steps/XykWizardStep4.tsx index 6a74d47..1979fbd 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep4.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep4.tsx @@ -1,7 +1,8 @@ import { Button } from '@/components/ui/button'; import { emptyFn } from '@/lib/shared'; import { accountIdToBech32 } from '@/lib/utils'; -import { Check } from 'lucide-react'; +import { getMidenscanAccountUrl } from '@/hooks/useLaunchpad'; +import { Check, ExternalLink } from 'lucide-react'; import { useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { XYK_WIZARD_STORAGE_KEY, type XykStepProps } from '../XykWizard'; @@ -14,7 +15,12 @@ function clearPersistedWizard() { } } -const XykStep4 = ({ form, tokenMetadata, restart }: XykStepProps) => { +const XykStep4 = ({ + form, + tokenMetadata, + restart, + lastDeployedPoolIdBech32, +}: XykStepProps) => { const tokenA = useMemo(() => { return tokenMetadata[form.tokenA ? accountIdToBech32(form.tokenA) : '']; }, [form.tokenA, tokenMetadata]); @@ -38,19 +44,46 @@ const XykStep4 = ({ form, tokenMetadata, restart }: XykStepProps) => {

Pool {poolName} created successfully!

-

- View your Pool at Address: (saved to Your pools) -

+ {lastDeployedPoolIdBech32 && ( +
+ Pool address + + {lastDeployedPoolIdBech32} + + + View on MidenScan + + +
+ )}
- - - + {lastDeployedPoolIdBech32 + ? ( + + + + ) + : ( + + + + )} + )} + + {isDeployed ? 'Pool' : 'Draft'} + +
{p.tokenA.name} · {p.tokenB.name} @@ -127,8 +164,25 @@ export default function Pools() { {new Date(p.createdAt).toLocaleString()}
- - ))} + ); + return ( + + {isDeployed + ? ( + + {cardContent} + + ) + : cardContent} + + ); + })}
)} diff --git a/src/providers/UnifiedWalletProvider.tsx b/src/providers/UnifiedWalletProvider.tsx index c0261ec..03acc73 100644 --- a/src/providers/UnifiedWalletProvider.tsx +++ b/src/providers/UnifiedWalletProvider.tsx @@ -53,15 +53,14 @@ export function UnifiedWalletProvider({ children }: UnifiedWalletProviderProps) if (walletType === 'miden') { // Delegate to Miden wallet adapter if ('type' in tx && tx.type === TransactionType.Custom) { - return midenWallet.requestTransaction?.({ + const txId = await midenWallet.requestTransaction?.({ type: TransactionType.Custom, payload: tx.payload, }); - } - return midenWallet.requestTransaction?.(tx); + return txId; + } else throw new Error('Unsupported transaction type for Miden wallet'); } else if (walletType === 'para' && paraMidenClient && paraMidenAccountId) { // For Para users, execute transactions via the Miden client. - // Use mutex to prevent concurrent access to the client. return clientMutex.runExclusive(async () => { if ('type' in tx && tx.type === TransactionType.Custom) { const customTx = tx.payload as { transactionRequest: string }; From 8fa54b4bd6fbc9cc97c2e48728d27c5d5ee90a36 Mon Sep 17 00:00:00 2001 From: mico Date: Mon, 9 Mar 2026 12:27:19 +0100 Subject: [PATCH 12/49] Network acc like in tut --- src/lib/DeployXykPool.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 8b73db2..16f4bd0 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -138,10 +138,11 @@ export async function deployNewPool({ const authComponent = AccountComponent.createAuthComponentFromSecretKey(secretKey); const contract = new AccountBuilder(walletSeed) - .accountType(AccountType.RegularAccountUpdatableCode) + .accountType(AccountType.RegularAccountImmutableCode) .storageMode(AccountStorageMode.network()) + .withNoAuthComponent() .withComponent(lp_local_component) - .withAuthComponent(authComponent) + // .withAuthComponent(authComponent) // .withComponent(c_prod_pool_component) .withBasicWalletComponent() .build(); From 4506c948c4318ae8c594c09d88dfb40782806028 Mon Sep 17 00:00:00 2001 From: mico Date: Mon, 9 Mar 2026 13:52:19 +0100 Subject: [PATCH 13/49] init script --- src/lib/DeployXykPool.ts | 37 +++++++++++++++++++++++++++++++-- src/masm/accounts/lp_local.masm | 8 ++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 16f4bd0..1d07704 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -66,6 +66,19 @@ const build_c_prod_lib = (client: WebClient) => { return builder.buildLibrary('zoro::c_prod_pool', c_prod); }; +const init_script = (client: WebClient) => { + const scriptCode = ` + use zoro::lp_local + begin + exec.lp_local::init + end + `; + const lp_local = build_lp_local_lib(client); + const builder = client.createCodeBuilder(); + builder.linkStaticLibrary(lp_local); + return builder.compileTxScript(scriptCode); +}; + export async function deployNewPool({ client, token0, @@ -89,6 +102,15 @@ export async function deployNewPool({ const assets_mapping_slot = StorageSlot.map('zoro::lp_local::assets_mapping', assets); const reserve_slot = StorageSlot.emptyValue('zoro::lp_local::reserve'); + const init_slot = StorageSlot.fromValue( + 'zoro::lp_local::is_init', + Word.newFromFelts([ + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(BigInt(0)), + ]), + ); const total_supply_slot = StorageSlot.emptyValue('zoro::lp_local::total_supply'); const user_deposits_mapping = new StorageMap(); user_deposits_mapping.insert( @@ -127,6 +149,7 @@ export async function deployNewPool({ reserve_slot, total_supply_slot, user_deposits_slot, + init_slot, ], ).withSupportsAllTypes(); @@ -140,9 +163,9 @@ export async function deployNewPool({ const contract = new AccountBuilder(walletSeed) .accountType(AccountType.RegularAccountImmutableCode) .storageMode(AccountStorageMode.network()) - .withNoAuthComponent() + // .withNoAuthComponent() .withComponent(lp_local_component) - // .withAuthComponent(authComponent) + .withAuthComponent(authComponent) // .withComponent(c_prod_pool_component) .withBasicWalletComponent() .build(); @@ -151,9 +174,19 @@ export async function deployNewPool({ contract.account.id(), secretKey, ); + + await client.importAccountById(contract.account.id()); + await client.syncState(); console.log('Deployed new XYK pool at: ', accountIdToBech32(contract.account.id())); + const tx_script = init_script(client); + const initTx = new TransactionRequestBuilder() + .withCustomScript(tx_script) + .build(); + await client.submitNewTransaction(contract.account.id(), initTx); + await client.syncState(); + return { newPoolId: contract.account.id(), }; diff --git a/src/masm/accounts/lp_local.masm b/src/masm/accounts/lp_local.masm index 4f7bdcd..16ccc26 100644 --- a/src/masm/accounts/lp_local.masm +++ b/src/masm/accounts/lp_local.masm @@ -32,6 +32,7 @@ const ASSETS_MAPPING_SLOT = word("zoro::lp_local::assets_mapping") const RESERVE_SLOT = word("zoro::lp_local::reserve") const TOTAL_SUPPLY_SLOT = word("zoro::lp_local::total_supply") const USER_DEPOSITS_MAPPING_SLOT = word("zoro::lp_local::user_deposits_mapping") +const IS_INIT = word("zoro::lp_local::is_init") ################# #! Computes the LP amount out for a deposit of amount_0 and amount_1. @@ -584,7 +585,6 @@ end # # => [] #end - #! Adds the provided asset to the active account. #! #! Inputs: [ASSET, pad(4, pad(8)] @@ -608,3 +608,9 @@ end # # # => [pad(16)] #end + +pub proc init + push.1 + push.IS_INIT[0..2] + exec.native_account::set_map_item +end From 42f552476ef7b1a5e48b84a188e816203586cdf0 Mon Sep 17 00:00:00 2001 From: mico Date: Wed, 11 Mar 2026 10:34:28 +0100 Subject: [PATCH 14/49] Spawning xyk pools successful --- src/components/xyk-wizard/XykWizard.tsx | 13 +++++++++- src/hooks/useXykPool.tsx | 0 src/lib/DeployXykPool.ts | 34 ++++--------------------- src/lib/XykDepositNote.ts | 10 ++++++-- src/masm/accounts/lp_local.masm | 6 ----- 5 files changed, 25 insertions(+), 38 deletions(-) create mode 100644 src/hooks/useXykPool.tsx diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index d121850..2e54c4d 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -15,7 +15,12 @@ import { compileXykDepositTransaction } from '@/lib/XykDepositNote'; import { type TokenConfigWithBalance, ZoroContext } from '@/providers/ZoroContext'; import { type TokenConfig } from '@/providers/ZoroProvider'; import { TransactionType } from '@demox-labs/miden-wallet-adapter'; -import { AccountId } from '@miden-sdk/miden-sdk'; +import { + AccountId, + NoteAndArgs, + NoteAndArgsArray, + TransactionRequestBuilder, +} from '@miden-sdk/miden-sdk'; import { AlertCircle, ChevronLeft, Loader2 } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -216,6 +221,7 @@ const XykWizard = () => { } const { newPoolId } = await deployNewPool({ client, token0, token1 }); + const { tx } = await compileXykDepositTransaction({ token0, token1, @@ -230,6 +236,11 @@ const XykWizard = () => { payload: tx, }); await client.syncState(); + // const consumeTx = new TransactionRequestBuilder().withInputNotes( + // new NoteAndArgsArray([new NoteAndArgs(note, null)]), + // ).build(); + // await client.submitNewTransaction(newPoolId, consumeTx); + // await client.syncState(); console.log('Deposited, tx of deposit: ', txId); return newPoolId; } catch (e) { diff --git a/src/hooks/useXykPool.tsx b/src/hooks/useXykPool.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 1d07704..727c69f 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -66,19 +66,6 @@ const build_c_prod_lib = (client: WebClient) => { return builder.buildLibrary('zoro::c_prod_pool', c_prod); }; -const init_script = (client: WebClient) => { - const scriptCode = ` - use zoro::lp_local - begin - exec.lp_local::init - end - `; - const lp_local = build_lp_local_lib(client); - const builder = client.createCodeBuilder(); - builder.linkStaticLibrary(lp_local); - return builder.compileTxScript(scriptCode); -}; - export async function deployNewPool({ client, token0, @@ -102,15 +89,6 @@ export async function deployNewPool({ const assets_mapping_slot = StorageSlot.map('zoro::lp_local::assets_mapping', assets); const reserve_slot = StorageSlot.emptyValue('zoro::lp_local::reserve'); - const init_slot = StorageSlot.fromValue( - 'zoro::lp_local::is_init', - Word.newFromFelts([ - new Felt(BigInt(0)), - new Felt(BigInt(0)), - new Felt(BigInt(0)), - new Felt(BigInt(0)), - ]), - ); const total_supply_slot = StorageSlot.emptyValue('zoro::lp_local::total_supply'); const user_deposits_mapping = new StorageMap(); user_deposits_mapping.insert( @@ -149,7 +127,6 @@ export async function deployNewPool({ reserve_slot, total_supply_slot, user_deposits_slot, - init_slot, ], ).withSupportsAllTypes(); @@ -158,14 +135,15 @@ export async function deployNewPool({ const walletSeed = new Uint8Array(32); crypto.getRandomValues(walletSeed); const secretKey = AuthSecretKey.rpoFalconWithRNG(walletSeed); - const authComponent = AccountComponent.createAuthComponentFromSecretKey(secretKey); + // const authComponent = AccountComponent.createAuthComponentFromSecretKey(secretKey); const contract = new AccountBuilder(walletSeed) .accountType(AccountType.RegularAccountImmutableCode) .storageMode(AccountStorageMode.network()) - // .withNoAuthComponent() + // .storageMode(AccountStorageMode.public()) + .withNoAuthComponent() .withComponent(lp_local_component) - .withAuthComponent(authComponent) + // .withAuthComponent(authComponent) // .withComponent(c_prod_pool_component) .withBasicWalletComponent() .build(); @@ -175,14 +153,12 @@ export async function deployNewPool({ secretKey, ); - await client.importAccountById(contract.account.id()); + await client.newAccount(contract.account, true); await client.syncState(); console.log('Deployed new XYK pool at: ', accountIdToBech32(contract.account.id())); - const tx_script = init_script(client); const initTx = new TransactionRequestBuilder() - .withCustomScript(tx_script) .build(); await client.submitNewTransaction(contract.account.id(), initTx); await client.syncState(); diff --git a/src/lib/XykDepositNote.ts b/src/lib/XykDepositNote.ts index 998afde..f984a35 100644 --- a/src/lib/XykDepositNote.ts +++ b/src/lib/XykDepositNote.ts @@ -6,6 +6,8 @@ import { MidenArrays, Note, NoteAssets, + NoteAttachment, + NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, @@ -52,14 +54,18 @@ export async function compileXykDepositTransaction({ DEPOSIT_SCRIPT, ); - // Note should only contain the offered asset const noteTag = NoteTag.withAccountTarget(poolAccountId); + const attachment = NoteAttachment.newNetworkAccountTarget( + poolAccountId, + NoteExecutionHint.always(), + ); + const metadata = new NoteMetadata( userAccountId, NoteType.Public, noteTag, - ); + ).withAttachment(attachment); const inputs = new NoteInputs( new FeltArray([ diff --git a/src/masm/accounts/lp_local.masm b/src/masm/accounts/lp_local.masm index 16ccc26..c189cbb 100644 --- a/src/masm/accounts/lp_local.masm +++ b/src/masm/accounts/lp_local.masm @@ -608,9 +608,3 @@ end # # # => [pad(16)] #end - -pub proc init - push.1 - push.IS_INIT[0..2] - exec.native_account::set_map_item -end From d6ab887d55fb35375421f8f5d2f1c672bbf110b7 Mon Sep 17 00:00:00 2001 From: mico Date: Wed, 11 Mar 2026 10:36:11 +0100 Subject: [PATCH 15/49] Updated with latest masm --- src/masm/accounts/c_prod_pool.masm | 74 +++++++++++++++++++----- src/masm/accounts/lp_local.masm | 90 +++++++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 22 deletions(-) diff --git a/src/masm/accounts/c_prod_pool.masm b/src/masm/accounts/c_prod_pool.masm index aaa7f30..443d859 100644 --- a/src/masm/accounts/c_prod_pool.masm +++ b/src/masm/accounts/c_prod_pool.masm @@ -14,7 +14,6 @@ use zoro::math use zoro::lp_local use lp_local::set_reserves -pub use lp_local::get_reserves pub use lp_local::get_pool_asset_ids use lp_local::add_to_reserve_by_asset_id use lp_local::sub_from_reserve_by_asset_id @@ -60,7 +59,7 @@ const RETURN_NOTE_AMOUNT = 43 #@todo factor out fee amount and make it configurable @locals(6) -pub proc get_amount_out_u64(amount_in: felt, reserve_out: felt, reserve_in: felt) -> felt +pub proc get_amount_out_u64(amount_in: felt, reserve_in: felt, reserve_out: felt) -> felt # loc.0: reserve_in # loc.1: reserve_out # loc.2: amount_in @@ -68,7 +67,7 @@ pub proc get_amount_out_u64(amount_in: felt, reserve_out: felt, reserve_in: felt # loc.4: amount_in_with_fee_high # loc.5: amount_in_with_fee_low - loc_store.0 loc_store.1 loc_store.2 + loc_store.2 loc_store.0 loc_store.1 # amount_in loc_load.2 u32split @@ -102,10 +101,50 @@ pub proc get_amount_out_u64(amount_in: felt, reserve_out: felt, reserve_in: felt exec.sys::truncate_stack end +@locals(3) +pub proc get_amount_in_u64(amount_out: felt, reserve_in: felt, reserve_out: felt) -> felt + # loc.0: reserve_in + # loc.1: reserve_out + # loc.2: amount_out + + loc_store.2 loc_store.0 loc_store.1 + + # amount_out + loc_load.2 u32split + # => [amount_out_high, amount_out_low] + push.1000 u32split + # => [1000_high, 1000_low, amount_out_high, amount_out_low] + # @note: consider overflowing mul + exec.u64::wrapping_mul + # => [amount_out_scaled_high, amount_out_scaled_low] + ## reserve_in + loc_load.0 u32split + # => [reserve_in_high, reserve_in_low, amount_out_scaled_high, amount_out_scaled_low] + # numerator = amount_out_scaled * reserve_in + exec.u64::wrapping_mul + # => [numerator_scaled_high, numerator_scaled_low] + ## denominator = (reserve_out - amount_out) * (1-fee) + ## reserve_out + loc_load.1 u32split + # => [reserve_out_high, reserve_out_low, numerator_scaled_high, numerator_scaled_low] + ## amount_out + loc_load.2 u32split + # => [amount_out_high, amount_out_low, reserve_out_high, reserve_out_low, numerator_scaled_high, numerator_scaled_low] + exec.u64::wrapping_sub + # => [denominator_high, denominator_low, numerator_scaled_high, numerator_scaled_low] + push.997 u32split exec.u64::wrapping_mul + # => [denominator_with_fee_high, denominator_with_fee_low, numerator_high, numerator_low] + exec.u64::div + # => [amount_in_high, amount_in_low] + exec.math::safe_cast_u64_into_felt + # => [amount_in] + exec.sys::truncate_stack +end + @locals(2) -pub proc do_swap(asset_in: word, min_asset_out: word, deadline: felt, note_tag: felt, note_type: felt, return_note_recipient: word) -> felt +pub proc swap_exact_tokens_for_tokens(asset_in: word, min_asset_out: word, deadline: felt, note_tag: felt, note_type: felt, return_note_recipient: word) -> felt # loc.0: deadline # loc.1: amount_out ### localy store asset in, min asset out, and deadline @@ -114,27 +153,37 @@ pub proc do_swap(asset_in: word, min_asset_out: word, deadline: felt, note_tag: loc_store.0 ### store note metadata in memory mem_store.RETURN_NOTE_TAG mem_store.RETURN_NOTE_TYPE mem_storew_le.RETURN_NOTE_RECIPIENT_WORD dropw - mem_load.ASSET_IN_ID_SUFFIX mem_load.ASSET_IN_ID_PREFIX exec.get_reserve_by_asset_id - # => [reserve_in] - mem_load.ASSET_OUT_ID_SUFFIX mem_load.ASSET_OUT_ID_PREFIX exec.get_reserve_by_asset_id - # => [reserve_out, reserve_in] + mem_load.ASSET_OUT_ID_SUFFIX mem_load.ASSET_OUT_ID_PREFIX + exec.get_reserve_by_asset_id + # => [reserve_out] + mem_load.ASSET_IN_ID_SUFFIX mem_load.ASSET_IN_ID_PREFIX + exec.get_reserve_by_asset_id + # => [reserve_in, reserve_out] mem_load.AMOUNT_IN - # => [amount_in, reserve_out, reserve_in] + # => [amount_in, reserve_in, reserve_out] exec.get_amount_out_u64 loc_store.1 loc_load.1 # => [amount_out] mem_load.AMOUNT_OUT # => [min_amount_out, amount_out] gte if.true # sufficient amount out - ### send asset out to user + ### set asset out to be sent to user padw mem_loadw_le.ASSET_OUT_WORD mem_storew_le.RETURN_NOTE_ASSET_WORD dropw - ### add ASSEET IN to pool + ### add ASSEET IN to the pool padw mem_loadw_le.ASSET_IN_WORD exec.native_account::add_asset dropw # => [] ### update reserve - + padw mem_loadw_le.ASSET_OUT_WORD + # => [asset_out_id_prefix, asset_out_id_suffix, 0, amount_out] + movup.3 + # => [amount_out, asset_out_id_prefix, asset_out_id_suffix] + exec.sub_from_reserve_by_asset_id + padw mem_loadw_le.ASSET_IN_WORD movup.3 + # => [amount_in, asset_in_id_prefix, asset_in_id_suffix] + exec.add_to_reserve_by_asset_id + # => [] else # insufficient amount out ### send asset in back to user padw mem_loadw_le.ASSET_IN_WORD @@ -142,7 +191,6 @@ pub proc do_swap(asset_in: word, min_asset_out: word, deadline: felt, note_tag: end exec._create_p2id_note - exec.sys::truncate_stack end diff --git a/src/masm/accounts/lp_local.masm b/src/masm/accounts/lp_local.masm index c189cbb..0aa88a2 100644 --- a/src/masm/accounts/lp_local.masm +++ b/src/masm/accounts/lp_local.masm @@ -32,7 +32,6 @@ const ASSETS_MAPPING_SLOT = word("zoro::lp_local::assets_mapping") const RESERVE_SLOT = word("zoro::lp_local::reserve") const TOTAL_SUPPLY_SLOT = word("zoro::lp_local::total_supply") const USER_DEPOSITS_MAPPING_SLOT = word("zoro::lp_local::user_deposits_mapping") -const IS_INIT = word("zoro::lp_local::is_init") ################# #! Computes the LP amount out for a deposit of amount_0 and amount_1. @@ -137,9 +136,9 @@ pub proc get_reserves#() -> (felt, felt) drop drop end -pub proc set_reserves#(reserve_0: felt, reserve_1: felt) +pub proc set_reserves(reserve_0: felt, reserve_1: felt) ### amount validation vs vault - push.0.0push.RESERVE_SLOT[0..2] exec.native_account::set_item + push.0.0 push.RESERVE_SLOT[0..2] exec.native_account::set_item dropw end @@ -236,10 +235,12 @@ pub proc add_to_reserves#(amount_0: felt, amount_1: felt) swap # => [new_reserve_0, new_reserve_1] exec.set_reserves + # => [] + exec.sys::truncate_stack end @locals(2) -proc sub_from_reserves#(amount_0: felt, amount_1: felt) +pub proc sub_from_reserves#(amount_0: felt, amount_1: felt) # loc.0: amount_0 # loc.1: amount_1 loc_store.0 loc_store.1 @@ -256,13 +257,83 @@ proc sub_from_reserves#(amount_0: felt, amount_1: felt) exec.set_reserves end -pub proc add_to_reserve_by_asset_id#(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) - drop drop drop +@locals(4) +pub proc set_reserve_by_asset_id(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) + # loc.0: amount + # loc.1: asset_id_prefix + # loc.2: asset_id_suffix + # loc.3: asset_index + loc_store.0 loc_store.1 loc_store.2 + # => [] + loc_load.2 loc_load.1 + # => [asset_id_prefix, asset_id_suffix] + exec.get_asset_index loc_store.3 + exec.get_reserves + # => [reserve_0, reserve_1] + loc_load.3 + # => [asset_index, reserve_0, reserve_1] + ### if asset_index == 0 set reserve_0 + eq.0 if.true + # => [reserve_0, reserve_1] + drop loc_load.0 + # => [new_reserve_0, reserve_1] + else ### check if asset_index == 1 + # => [reserve_0, reserve_1] + loc_load.3 eq.1 if.true + # => [reserve_0, reserve_1] + loc_load.0 + # => [new_reserve_1, reserve_0, reserve_1] + swap.2 drop + # => [reserve_0, new_reserve_1] + else + ### asset index not found + assert.err=ERR_UNKNOWN_ASSET + end + end + exec.set_reserves + # => [] exec.sys::truncate_stack end -proc sub_from_reserve_by_asset_id#(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) - drop drop drop - exec.sys::truncate_stack + +@locals(4) +pub proc add_to_reserve_by_asset_id(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) + # loc.0: amount + # loc.1: asset_id_prefix + # loc.2: asset_id_suffix + # loc.3: new_reserve + loc_store.0 loc_store.1 loc_store.2 + # => [] + loc_load.2 loc_load.1 + # => [asset_id_prefix, asset_id_suffix] + exec.get_reserve_by_asset_id loc_load.0 + # => [amount, reserve] + exec.math::safe_add + # => [new_reserve] + loc_store.3 + # => [] + loc_load.2 loc_load.1 loc_load.3 + # => [ new_reserve, asset_id_suffix, asset_id_prefix] + exec.set_reserve_by_asset_id + # => [] +end + +@locals(3) +pub proc sub_from_reserve_by_asset_id(amount: felt, asset_id_prefix: felt, asset_id_suffix: felt) + # loc.0: amount + # loc.1: asset_id_prefix + # loc.2: asset_id_suffix + loc_store.0 loc_store.1 loc_store.2 + # => [] + loc_load.2 loc_load.1 + # => [asset_id_prefix, asset_id_suffix] + exec.get_reserve_by_asset_id loc_load.0 + # => [amount, reserve] + exec.math::safe_sub + # => [new_reserve] + loc_load.2 loc_load.1 movup.2 + # => [ new_reserve, asset_id_suffix, asset_id_prefix] + exec.set_reserve_by_asset_id + # => [] end @locals(3) @@ -585,6 +656,7 @@ end # # => [] #end + #! Adds the provided asset to the active account. #! #! Inputs: [ASSET, pad(4, pad(8)] From b8d12a9687a7ab0792e8f579369c6af72e71eb8b Mon Sep 17 00:00:00 2001 From: Solo <83204015+dotsolo@users.noreply.github.com> Date: Wed, 11 Mar 2026 05:45:49 -0400 Subject: [PATCH 16/49] Update swap UI design (#60) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update swap UI design — card styling, spacing, and slippage panel - Sell card: white bg with border, buy card: gray bg no border - Larger rounded corners, more padding, tighter card gap - Swap arrows: original SVG in smaller rounded box with white buffer - Settings gear: larger icon, slippage panel aligned to card edge - Simplified slippage panel: cleaner layout, bigger input - Lightened background dots - Moved "Powered by Miden" between content and footer - Bigger, bolder Connect Wallet button Co-Authored-By: Claude Opus 4.6 * Center swap UI vertically on screen Co-Authored-By: Claude Opus 4.6 * Mobile responsive: hamburger nav, smaller wallet button, tighter swap spacing - Header: hamburger menu on mobile, centered wallet button - Wallet button: smaller text/padding/icons on mobile - Swap cards: responsive padding and text sizes - Gear icon: scales down on mobile --- src/components/Header.tsx | 62 +++++++- src/components/Slippage.tsx | 98 ++++-------- src/components/SwapInputBuy.tsx | 2 +- src/components/SwapPairs.tsx | 41 +---- src/components/UnifiedWalletButton.tsx | 14 +- src/index.css | 2 +- src/pages/Swap.tsx | 206 ++++++++++++------------- 7 files changed, 197 insertions(+), 228 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index cced462..312996e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,9 +1,12 @@ +import { Menu, X } from 'lucide-react'; +import { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { ModeToggle } from './ModeToggle'; import { UnifiedWalletButton } from './UnifiedWalletButton'; export function Header() { const location = useLocation(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const navLinkClass = (path: string) => `px-4 py-2 rounded-md text-sm font-medium transition-colors h-10 inline-flex items-center ${ @@ -12,9 +15,17 @@ export function Header() { : 'text-muted-foreground hover:text-foreground' }`; + const mobileNavLinkClass = (path: string) => + `block px-4 py-3 text-base font-medium transition-colors ${ + location.pathname === path || location.pathname.startsWith(path + '/') + ? 'text-foreground' + : 'text-muted-foreground hover:text-foreground' + }`; + return ( -
-
+
+ {/* Desktop */} +
+ + {/* Mobile */} +
+ + Zoro + +
+ +
+ +
+ + {/* Mobile menu dropdown */} + {mobileMenuOpen && ( + + )}
); } diff --git a/src/components/Slippage.tsx b/src/components/Slippage.tsx index 9338280..63a489f 100644 --- a/src/components/Slippage.tsx +++ b/src/components/Slippage.tsx @@ -1,7 +1,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; -import { Info, Settings, X } from 'lucide-react'; +import { Settings, X } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; interface SlippageProps { @@ -39,102 +39,58 @@ const Slippage = ({ slippage, onSlippageChange }: SlippageProps) => { }, [slippage]); return ( -
+ <> {isOpen && ( <> - {/* Backdrop */}
- {/* Settings Panel */} - - - {/* Header */} + +
-
-

Max slippage

-
- - {/* Tooltip */} -
-
- Your transaction will revert if the price changes unfavorably by - more than this percentage -
-
-
-
-
-
- + +
- {/* Slippage Input */} -
-
- handleSlippageChange(e.target.value)} - className='text-center text-sm pr-8' - min='0' - max='50' - step='0.1' - placeholder='0.5' - /> - - % - -
- - {/* Conditional Warnings */} - {slippage > 5 && ( -
- High slippage risk -
- )} - - {slippage < 0.1 && slippage > 0 && ( -
- May fail due to low slippage -
- )} +
+ handleSlippageChange(e.target.value)} + className='text-center text-lg font-medium pr-10 h-12 rounded-xl' + min='0' + max='50' + step='0.1' + placeholder='0.5' + /> + + % +
- - )} -
+ ); }; diff --git a/src/components/SwapInputBuy.tsx b/src/components/SwapInputBuy.tsx index 8f43fdf..46e11d3 100644 --- a/src/components/SwapInputBuy.tsx +++ b/src/components/SwapInputBuy.tsx @@ -51,7 +51,7 @@ const SwapInputBuy = ( value={stringBuy} disabled placeholder='0' - className='border-none text-3xl sm:text-4xl font-light outline-none flex-1 p-0 h-auto focus-visible:ring-0 no-spinner bg-transparent' + className='border-none text-4xl sm:text-6xl font-semibold text-muted-foreground outline-none flex-1 p-0 h-auto focus-visible:ring-0 no-spinner bg-transparent placeholder:text-muted-foreground' /> ); }, [stringBuy]); diff --git a/src/components/SwapPairs.tsx b/src/components/SwapPairs.tsx index 88a59f9..b8de0ef 100644 --- a/src/components/SwapPairs.tsx +++ b/src/components/SwapPairs.tsx @@ -5,48 +5,19 @@ export const SwapPairs = ( ); diff --git a/src/components/UnifiedWalletButton.tsx b/src/components/UnifiedWalletButton.tsx index e4c0a2e..cacce2e 100644 --- a/src/components/UnifiedWalletButton.tsx +++ b/src/components/UnifiedWalletButton.tsx @@ -61,25 +61,25 @@ export function UnifiedWalletButton({ className }: UnifiedWalletButtonProps) {
{showDropdown && ( diff --git a/src/index.css b/src/index.css index 7e64e30..c4449ba 100644 --- a/src/index.css +++ b/src/index.css @@ -40,7 +40,7 @@ --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; --footer: 0, 0%, 98%; - --bg-dots: 130, 15%, 92.2%; + --bg-dots: 130, 10%, 96%; } .dark { diff --git a/src/pages/Swap.tsx b/src/pages/Swap.tsx index dfac49a..2df9a41 100644 --- a/src/pages/Swap.tsx +++ b/src/pages/Swap.tsx @@ -251,121 +251,106 @@ function Swap() {
-
-
- {/* Sell Card */} - - -

Swap Tokens

-
-
-
Sell
- +
+
+

Swap Tokens

+ + {/* Settings gear - top right, relative so dropdown aligns to full width */} +
+ +
+ + {/* Sell Card — white bg, border, no shadow */} + + +
Sell
+
+ onInputChange(e.target.value)} + placeholder='0' + aria-errormessage={sellInputError} + className={`border-none bg-transparent text-4xl sm:text-6xl font-semibold text-foreground outline-none flex-1 p-0 h-auto focus-visible:ring-0 no-spinner placeholder:text-foreground/70 ${ + sellInputError + ? 'text-orange-600 placeholder:text-destructive/50' + : '' + }`} + /> +
+ setAsset('sell', id)} + excludeFaucetIdBech32={selectedAssetBuy?.faucetIdBech32} + />
- - -
- onInputChange(e.target.value)} - placeholder='0' - aria-errormessage={sellInputError} - className={`border-none text-3xl sm:text-4xl font-light outline-none flex-1 p-0 h-auto focus-visible:ring-0 no-spinner ${ +
+ {sellInputError && ( +

+ {sellInputError} +

+ )} +
+
+
+ {rawSell > BigInt(0) && selectedAssetSell + ? ( + <> + $ + + ) + : '$0'} +
+ {accountId && balanceSell !== null && balanceSell !== undefined + && ( +
- {sellInputError && ( -
-

- {sellInputError} -

-
+ > + {balanceSellFmt} {selectedAssetSell?.symbol ?? ''} + )} -
-
- {rawSell > BigInt(0) && selectedAssetSell && ( - <> - = $ - - - )} -
- {accountId && balanceSell !== null && balanceSell !== undefined - && ( -
- -
- )} -
- - +
{/* Swap Pairs */} -
- +
+
+ +
- {/* Buy Card */} - - -
-
Buy
- - -
- -
- setAsset('buy', id)} - excludeFaucetIdBech32={selectedAssetSell?.faucetIdBech32} - /> -
-
-
- {balancebuy !== null && balancebuy !== undefined && ( -
- {balanceBuyFmt} {selectedAssetBuy?.symbol ?? ''} -
- )} -
-
-
+ {/* Buy Card — gray bg, no border, no shadow */} + + +
Sell
+
+ +
+ setAsset('buy', id)} + excludeFaucetIdBech32={selectedAssetSell?.faucetIdBech32} + /> +
{/* Main Action Button */} -
+
{connected ? ( ) : ( -
+
{connecting && ( )}
- +
)}
-

+

{selectedAssetBuy && selectedAssetSell ? ( @@ -432,12 +417,11 @@ function Swap() { ) : null}

- {/* Powered by MIDEN */} -
- {poweredByMiden} -
+
+ {poweredByMiden} +
{isSuccessModalOpen && ( Date: Wed, 11 Mar 2026 20:24:39 +0100 Subject: [PATCH 17/49] Split pool details, new withdraw & swap notes for xyk --- src/App.tsx | 9 +- src/components/AssetIcon.tsx | 33 +- src/components/IlRiskCard.tsx | 21 + src/components/PoolCompositionCard.tsx | 114 ++++ src/components/PoolDetailHeader.tsx | 63 +++ src/components/PoolDetailLayout.tsx | 37 ++ src/components/PoolDetailStats.tsx | 81 +++ src/components/PoolInfoCard.tsx | 43 ++ src/components/PoolModal.tsx | 6 +- src/components/PriceTvlChartCard.tsx | 44 ++ src/components/RecentTransactionsCard.tsx | 60 ++ src/components/SwapPairs.tsx | 2 +- src/components/XykPairIcon.tsx | 17 +- src/components/XykPoolModal.tsx | 166 ++++++ src/components/xyk-wizard/XykWizard.tsx | 12 +- .../xyk-wizard/steps/XykWizardStep1.tsx | 64 ++- .../xyk-wizard/steps/XykWizardStep4.tsx | 2 +- src/hooks/useXykDeposit.ts | 81 +++ src/hooks/useXykLpBalance.ts | 61 ++ src/hooks/useXykPool.tsx | 167 ++++++ src/hooks/useXykPoolNotes.ts | 98 ++++ src/lib/DeployXykPool.ts | 6 +- src/lib/XykDepositNote.ts | 1 - src/lib/XykSwapNote.ts | 135 +++++ src/lib/XykWithdrawNote.ts | 127 +++++ src/lib/utils.ts | 18 +- .../{c_prod_pool.masm => xyk_pool.masm} | 106 +++- ... => xyk_swap_exact_tokens_for_tokens.masm} | 5 +- .../xyk_swap_tokens_for_exact_tokens.masm | 45 ++ src/pages/HfPoolDetail.tsx | 193 +++++++ src/pages/LiquidityPools.tsx | 2 +- src/pages/PoolDetail.tsx | 532 ------------------ src/pages/Pools.tsx | 2 +- src/pages/Swap.tsx | 37 +- src/pages/XykPoolDetail.tsx | 242 ++++++++ 35 files changed, 2008 insertions(+), 624 deletions(-) create mode 100644 src/components/IlRiskCard.tsx create mode 100644 src/components/PoolCompositionCard.tsx create mode 100644 src/components/PoolDetailHeader.tsx create mode 100644 src/components/PoolDetailLayout.tsx create mode 100644 src/components/PoolDetailStats.tsx create mode 100644 src/components/PoolInfoCard.tsx create mode 100644 src/components/PriceTvlChartCard.tsx create mode 100644 src/components/RecentTransactionsCard.tsx create mode 100644 src/components/XykPoolModal.tsx create mode 100644 src/hooks/useXykDeposit.ts create mode 100644 src/hooks/useXykLpBalance.ts create mode 100644 src/hooks/useXykPoolNotes.ts create mode 100644 src/lib/XykSwapNote.ts create mode 100644 src/lib/XykWithdrawNote.ts rename src/masm/accounts/{c_prod_pool.masm => xyk_pool.masm} (65%) rename src/masm/notes/{xyk_swap.masm => xyk_swap_exact_tokens_for_tokens.masm} (94%) create mode 100644 src/masm/notes/xyk_swap_tokens_for_exact_tokens.masm create mode 100644 src/pages/HfPoolDetail.tsx delete mode 100644 src/pages/PoolDetail.tsx create mode 100644 src/pages/XykPoolDetail.tsx diff --git a/src/App.tsx b/src/App.tsx index dea8198..7a93eda 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { WalletProvider, } from '@demox-labs/miden-wallet-adapter'; import { useMemo } from 'react'; -import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'; import NotFound from './pages/404'; import FaucetPage from './pages/Faucet'; import SwapPage from './pages/Swap'; @@ -15,7 +15,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Bounce, ToastContainer } from 'react-toastify'; import Launchpad from './pages/Launchpad'; import LiquidityPools from './pages/LiquidityPools'; -import PoolDetail from './pages/PoolDetail'; +import HfPoolDetail from './pages/HfPoolDetail'; +import XykPoolDetail from './pages/XykPoolDetail'; import NewXykPool from './pages/NewXykPool'; import Pools from './pages/Pools'; import ModalProvider from './providers/ModalProvider'; @@ -33,7 +34,9 @@ function AppRouter() { } /> } /> } /> - } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/AssetIcon.tsx b/src/components/AssetIcon.tsx index c865116..c8d73ed 100644 --- a/src/components/AssetIcon.tsx +++ b/src/components/AssetIcon.tsx @@ -8,20 +8,37 @@ interface AssetIconProps { /** LP tokens (zBTC, zUSDC) use the same icon as the underlying token (BTC, USDC). */ const iconSymbol = (s: string) => (s.startsWith('z') ? s.slice(1) : s); +/** Symbols that have a dedicated icon (must match .icon-* classes in CSS). */ +const SYMBOLS_WITH_ICONS = new Set(['BTC', 'USDC', 'ETH', 'ANY']); + const AssetIcon = ({ symbol, size = 'normal' }: AssetIconProps) => { - const iconSize = size === 'normal' - ? 32 - : size === 'small' - ? 24 - : typeof size === 'number' - ? size - : 32; + const iconSize = + size === 'normal' + ? 32 + : size === 'small' + ? 24 + : typeof size === 'number' + ? size + : 32; const symbolForIcon = iconSymbol(symbol); + const hasIcon = SYMBOLS_WITH_ICONS.has(symbolForIcon.toUpperCase()); + + if (hasIcon) { + return ( + + ); + } + + const letter = (symbolForIcon || '?')[0].toUpperCase(); return ( + {letter} ); }; diff --git a/src/components/IlRiskCard.tsx b/src/components/IlRiskCard.tsx new file mode 100644 index 0000000..12c1f1b --- /dev/null +++ b/src/components/IlRiskCard.tsx @@ -0,0 +1,21 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { AlertTriangle } from 'lucide-react'; + +export function IlRiskCard() { + return ( + + + + + IL Risk + + + +

+ This pool's tokens may have price correlation. Impermanent loss + is possible when prices move. Consider concentrated ranges carefully. +

+
+
+ ); +} diff --git a/src/components/PoolCompositionCard.tsx b/src/components/PoolCompositionCard.tsx new file mode 100644 index 0000000..706d4d9 --- /dev/null +++ b/src/components/PoolCompositionCard.tsx @@ -0,0 +1,114 @@ +import AssetIcon from '@/components/AssetIcon'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { prettyBigintFormat } from '@/lib/format'; + +export interface PoolCompositionCardHfProps { + variant: 'hf'; + symbol: string; +} + +export interface PoolCompositionCardXykProps { + variant: 'xyk'; + token0Symbol: string; + token1Symbol: string; + reserve0: bigint; + reserve1: bigint; + decimals0: number; + decimals1: number; +} + +export type PoolCompositionCardProps = + | PoolCompositionCardHfProps + | PoolCompositionCardXykProps; + +export function PoolCompositionCard(props: PoolCompositionCardProps) { + return ( + + + Pool Composition + + + {props.variant === 'hf' ? ( + <> +
+
+ + {props.symbol} +
+
+

+

Single-sided

+
+
+

+ hfAMM pools are single-sided. +

+ + ) : ( + <> +
+
+ + {props.token0Symbol} +
+
+

+ {prettyBigintFormat({ + value: props.reserve0, + expo: props.decimals0, + })} +

+

+
+
+
+
+ + {props.token1Symbol} +
+
+

+ {prettyBigintFormat({ + value: props.reserve1, + expo: props.decimals1, + })} +

+

+
+
+ {(() => { + const total = props.reserve0 + props.reserve1; + const reserve0Pct = + total > 0n + ? Number((props.reserve0 * 100n) / total) + : 50; + const reserve1Pct = + total > 0n + ? Number((props.reserve1 * 100n) / total) + : 50; + return ( + <> +
+
+
+
+

+ {total > 0n + ? `${props.token0Symbol} ${reserve0Pct}% · ${props.token1Symbol} ${reserve1Pct}%` + : '—'} +

+ + ); + })()} + + )} + + + ); +} diff --git a/src/components/PoolDetailHeader.tsx b/src/components/PoolDetailHeader.tsx new file mode 100644 index 0000000..d14e7e0 --- /dev/null +++ b/src/components/PoolDetailHeader.tsx @@ -0,0 +1,63 @@ +import { Button } from '@/components/ui/button'; +import { truncateId } from '@/lib/format'; +import { ExternalLink } from 'lucide-react'; +import type { ReactNode } from 'react'; + +export interface PoolDetailHeaderProps { + pairLabel: string; + feeTier: string; + poolIdBech32: string; + onAddLiquidity: () => void; + onWithdraw: () => void; + hasPosition: boolean; + /** Single icon (HF) or stacked pair (XYK). */ + headerIcons: ReactNode; +} + +export function PoolDetailHeader({ + pairLabel, + feeTier, + poolIdBech32, + onAddLiquidity, + onWithdraw, + hasPosition, + headerIcons, +}: PoolDetailHeaderProps) { + return ( +
+
+ {headerIcons} +
+

{pairLabel}

+ +
+
+
+ + +
+
+ ); +} diff --git a/src/components/PoolDetailLayout.tsx b/src/components/PoolDetailLayout.tsx new file mode 100644 index 0000000..feffeed --- /dev/null +++ b/src/components/PoolDetailLayout.tsx @@ -0,0 +1,37 @@ +import { Footer } from '@/components/Footer'; +import { Header } from '@/components/Header'; +import { ArrowLeft } from 'lucide-react'; +import type { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; + +export interface PoolDetailLayoutProps { + backTo: string; + backLabel?: string; + title?: string; + children: ReactNode; +} + +export function PoolDetailLayout({ + backTo, + backLabel = 'Back to pools', + title, + children, +}: PoolDetailLayoutProps) { + return ( +
+ {title && {title} - ZoroSwap} +
+
+ + + {backLabel} + + {children} +
+
+
+ ); +} diff --git a/src/components/PoolDetailStats.tsx b/src/components/PoolDetailStats.tsx new file mode 100644 index 0000000..16e1be4 --- /dev/null +++ b/src/components/PoolDetailStats.tsx @@ -0,0 +1,81 @@ +import { Card, CardContent } from '@/components/ui/card'; +import { cn } from '@/lib/utils'; + +export interface PoolDetailStatsProps { + /** Main stat value (e.g. TVL or LP supply). */ + tvlFormatted: string; + /** Main stat label (e.g. "Total Value Locked" or "Total LP Supply"). */ + mainLabel?: string; + /** Optional first card: label (e.g. "Price"). */ + priceLabel?: string; + /** Optional first card: value (e.g. "1 ETH = 2000 USDC"). */ + priceValue?: string; + /** HF only: saturation percentage. When null, the Saturation card is not rendered. */ + saturationPercent?: number | null; + saturationColorClass?: string; +} + +function getSaturationColorClass(pct: number): string { + if (pct < 15 || pct > 185) return 'text-red-600 border-red-600/30 bg-red-500/10'; + if ((pct >= 15 && pct < 30) || (pct >= 170 && pct <= 185)) { + return 'text-yellow-600 border-yellow-600/30 bg-yellow-500/10'; + } + if (pct >= 30 && pct < 170) return 'text-green-600 border-green-600/30 bg-green-500/10'; + return 'text-muted-foreground border-border bg-muted/30'; +} + +export function PoolDetailStats({ + tvlFormatted, + mainLabel = 'Total Value Locked', + priceLabel, + priceValue, + saturationPercent = null, + saturationColorClass, +}: PoolDetailStatsProps) { + const saturationColor = + saturationColorClass ?? + (saturationPercent != null ? getSaturationColorClass(saturationPercent) : ''); + + return ( +
+ {priceLabel != null && priceValue != null && ( + + +

+ {priceLabel} +

+

{priceValue}

+
+
+ )} + + +

+ {mainLabel} +

+

{mainLabel === 'Total Value Locked' ? `$${tvlFormatted}` : tvlFormatted}

+
+
+ {saturationPercent != null && ( + + +

+ Saturation +

+

+ + {saturationPercent.toFixed(2)}% + +

+
+
+ )} +
+ ); +} diff --git a/src/components/PoolInfoCard.tsx b/src/components/PoolInfoCard.tsx new file mode 100644 index 0000000..bd860e0 --- /dev/null +++ b/src/components/PoolInfoCard.tsx @@ -0,0 +1,43 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export interface PoolInfoRow { + label: string; + value: React.ReactNode; +} + +export interface PoolInfoCardProps { + tvlFormatted: string; + /** First row label (e.g. "Total Liquidity" or "Total LP Supply"). */ + firstRowLabel?: string; + /** Whether first row value is prefixed with $. */ + firstRowIsUsd?: boolean; + /** Extra rows (e.g. HF: Total Liabilities, Reserve). */ + extraRows?: PoolInfoRow[]; +} + +export function PoolInfoCard({ + tvlFormatted, + firstRowLabel = 'Total Liquidity', + firstRowIsUsd = true, + extraRows = [], +}: PoolInfoCardProps) { + return ( + + + Pool Info + + +
+ {firstRowLabel} + {firstRowIsUsd ? `$${tvlFormatted}` : tvlFormatted} +
+ {extraRows.map((row, i) => ( +
+ {row.label} + {row.value} +
+ ))} +
+
+ ); +} diff --git a/src/components/PoolModal.tsx b/src/components/PoolModal.tsx index fd164b3..b34267f 100644 --- a/src/components/PoolModal.tsx +++ b/src/components/PoolModal.tsx @@ -507,6 +507,9 @@ export default function PoolModal({
{minLpFormatted ?? '0.00'}
+ {expectedLpFormatted != null && expectedLpFormatted !== (minLpFormatted ?? '0.00') && ( +

Expected: {expectedLpFormatted}

+ )} {isHfAmm && (
Total Value @@ -611,12 +614,13 @@ export default function PoolModal({ ) : ( <> +

LP: {withdrawReceiveFormatted ?? '0'}

{pool.symbol}
- {minWithdrawAssetFormatted} + {withdrawAssetOutFormatted} (min: {minWithdrawAssetFormatted})
diff --git a/src/components/PriceTvlChartCard.tsx b/src/components/PriceTvlChartCard.tsx new file mode 100644 index 0000000..f7b83f9 --- /dev/null +++ b/src/components/PriceTvlChartCard.tsx @@ -0,0 +1,44 @@ +import { TradingViewCandlesChart } from '@/components/TradingViewCandlesChart'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import type { MockCandle } from '@/mocks/poolDetailMocks'; + +export type ChartRange = '1D' | '1W' | '1M' | 'ALL'; + +export interface PriceTvlChartCardProps { + candles: MockCandle[]; + chartRange: ChartRange; + onChartRangeChange: (range: ChartRange) => void; +} + +export function PriceTvlChartCard({ + candles, + chartRange, + onChartRangeChange, +}: PriceTvlChartCardProps) { + return ( + + + Price & TVL +
+ {(['1D', '1W', '1M', 'ALL'] as const).map((r) => ( + + ))} +
+
+ +
+ +
+
+
+ ); +} diff --git a/src/components/RecentTransactionsCard.tsx b/src/components/RecentTransactionsCard.tsx new file mode 100644 index 0000000..c8a223e --- /dev/null +++ b/src/components/RecentTransactionsCard.tsx @@ -0,0 +1,60 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import type { MockRecentTx } from '@/mocks/poolDetailMocks'; + +const TYPE_COLORS: Record = { + Swap: 'text-primary', + Add: 'text-green-600', + Remove: 'text-amber-600', +}; + +export interface RecentTransactionsCardProps { + transactions: MockRecentTx[]; +} + +export function RecentTransactionsCard({ transactions }: RecentTransactionsCardProps) { + return ( + + + + Recent Transactions + + + +
+ + + + + + + + + + + + {transactions.map((tx, i) => ( + + + + + + + + ))} + +
TypeAmount inAmount outAccountTime
+ {tx.type} + {tx.amountIn}{tx.amountOut} + {tx.account} + + {tx.timeAgo} +
+
+
+
+ ); +} diff --git a/src/components/SwapPairs.tsx b/src/components/SwapPairs.tsx index b8de0ef..ad47293 100644 --- a/src/components/SwapPairs.tsx +++ b/src/components/SwapPairs.tsx @@ -5,7 +5,7 @@ export const SwapPairs = ( +
+ + {mode === 'Deposit' ? ( +
+
+ +
+ setAmount0Str(e.target.value)} + className='flex-1' + /> +
+ + + {poolData.token0.symbol} + +
+
+
+
+ +
+ setAmount1Str(e.target.value)} + className='flex-1' + /> +
+ + + {poolData.token1.symbol} + +
+
+
+ {inputError && ( +

{inputError}

+ )} + +
+ ) : ( +
+

+ Your LP balance:{' '} + {formatTokenAmount({ + value: lpBalance, + expo: 18, + }) ?? '0'} +

+

+ Withdraw liquidity — coming soon. +

+
+ )} +
+ ); +} diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index 2e54c4d..ab8af2d 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -15,12 +15,7 @@ import { compileXykDepositTransaction } from '@/lib/XykDepositNote'; import { type TokenConfigWithBalance, ZoroContext } from '@/providers/ZoroContext'; import { type TokenConfig } from '@/providers/ZoroProvider'; import { TransactionType } from '@demox-labs/miden-wallet-adapter'; -import { - AccountId, - NoteAndArgs, - NoteAndArgsArray, - TransactionRequestBuilder, -} from '@miden-sdk/miden-sdk'; +import { AccountId } from '@miden-sdk/miden-sdk'; import { AlertCircle, ChevronLeft, Loader2 } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -46,6 +41,7 @@ function readPersistedWizard(): { const defaultForm: XykWizardForm = { amountA: BigInt(0), amountB: BigInt(0), + feeBps: 30, }; try { const raw = localStorage.getItem(XYK_WIZARD_STORAGE_KEY); @@ -87,9 +83,7 @@ function readPersistedWizard(): { // ignore } } - if (typeof f.feeBps === 'number' && f.feeBps > 0) { - form.feeBps = f.feeBps; - } + form.feeBps = 30; return { step, form }; } catch { return { step: 0, form: defaultForm }; diff --git a/src/components/xyk-wizard/steps/XykWizardStep1.tsx b/src/components/xyk-wizard/steps/XykWizardStep1.tsx index ee3c0dc..0a16350 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep1.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep1.tsx @@ -94,39 +94,47 @@ const XykStep1 = ( tolerance and strategy.

- {FEE_TIERS.map(({ bps, label, hint }) => ( - - ))} + + {label} + + + {hint} + + + ); + })}
+

+ Constant product pools are fixed to 30BP fee for the time being. +

); diff --git a/src/components/xyk-wizard/steps/XykWizardStep4.tsx b/src/components/xyk-wizard/steps/XykWizardStep4.tsx index 1979fbd..4466366 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep4.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep4.tsx @@ -65,7 +65,7 @@ const XykStep4 = ({
{lastDeployedPoolIdBech32 ? ( - + - -
-
- -
- - -

- Total Value Locked -

-

${tvlFormatted}

-
-
- {isHfAmm && saturationPercent != null && ( - - -

- Saturation -

-

- - {saturationPercent.toFixed(2)}% - -

-
-
- )} - - -

- APR (est.) -

-

-
-
- - -

- 24H Volume -

-

-
-
- - -

- 24H Fees -

-

-
-
-
- -
-
- - - - Pool Composition - - - - {isHfAmm - ? ( - <> -
-
- - {pool.symbol} -
-
-

-

Single-sided

-
-
-

- hfAMM pools are single-sided. -

- - ) - : ( - <> -
-
- - {pool.symbol} -
-
-

- {prettyBigintFormat({ - value: poolBalance.reserve, - expo: decimals, - })} -

-

-
-
-
-
- - USDC -
-
-

- {prettyBigintFormat({ - value: poolBalance.totalLiabilities, - expo: decimals, - })} -

-

-
-
- {(() => { - const total = poolBalance.reserve + poolBalance.totalLiabilities; - const reservePct = total > 0n - ? Number((poolBalance.reserve * 100n) / total) - : 50; - const liabPct = total > 0n - ? Number((poolBalance.totalLiabilities * 100n) / total) - : 50; - return ( - <> -
-
-
-
-

- {total > 0n - ? `${pool.symbol} ${reservePct}% · USDC ${liabPct}%` - : '—'} -

- - ); - })()} - - )} - - - - - - Pool Info - - -
- Fee Tier - {feeTier} -
-
- 7D Volume - -
-
- 24h Transactions - -
-
- Total Liquidity - ${tvlFormatted} -
- {isHfAmm && ( - <> -
- Total Liabilities - - {fullNumberBigintFormat({ - value: poolBalance.totalLiabilities, - expo: decimals, - })} - -
-
- Reserve - - {fullNumberBigintFormat({ - value: poolBalance.reserve, - expo: decimals, - })} - -
- - )} -
-
- - {!isHfAmm && ( - - - - - IL Risk - - - -

- This pool's tokens may have price correlation. Impermanent loss - is possible when prices move. Consider concentrated ranges carefully. -

-
-
- )} -
- -
- {!isHfAmm && ( - - - - Recent Transactions - - - -
- - - - - - - - - - - - {mockRecentTxs.map((tx, i) => ( - - - - - - - - ))} - -
TypeAmount inAmount outAccountTime
- {tx.type} - {tx.amountIn}{tx.amountOut} - {tx.account} - - {tx.timeAgo} -
-
-
-
- )} - - - - Price & TVL -
- {(['1D', '1W', '1M', 'ALL'] as const).map((r) => ( - - ))} -
-
- -
- -
-
-
-
-
-
-
- - {isSuccessModalOpen && ( - setIsSuccessModalOpen(false)} - swapResult={txResult} - lpDetails={lpDetails} - orderStatus={txResult?.noteId - ? orderStatus[txResult.noteId]?.status - : undefined} - /> - )} -
- ); -} diff --git a/src/pages/Pools.tsx b/src/pages/Pools.tsx index 74a2c87..8b5facf 100644 --- a/src/pages/Pools.tsx +++ b/src/pages/Pools.tsx @@ -173,7 +173,7 @@ export default function Pools() { {isDeployed ? ( {cardContent} diff --git a/src/pages/Swap.tsx b/src/pages/Swap.tsx index 2df9a41..6943795 100644 --- a/src/pages/Swap.tsx +++ b/src/pages/Swap.tsx @@ -21,7 +21,6 @@ import { bech32ToAccountId } from '@/lib/utils'; import { OracleContext, useOraclePrices } from '@/providers/OracleContext'; import { ZoroContext } from '@/providers/ZoroContext'; import { type TokenConfig } from '@/providers/ZoroProvider.tsx'; -import type { AccountId } from '@miden-sdk/miden-sdk'; import { Loader2 } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'react-toastify'; @@ -64,10 +63,7 @@ function Swap() { } = useBalance({ token: selectedAssetSell, }); - const { - balance: balancebuy, - formattedLong: balanceBuyFmt, - } = useBalance({ + useBalance({ token: selectedAssetBuy, }); @@ -261,9 +257,11 @@ function Swap() {
{/* Sell Card — white bg, border, no shadow */} - + -
Sell
+
+ Sell +
-
+
{/* Buy Card — gray bg, no border, no shadow */} - + -
Sell
+
+ Buy +
{ }; const setLocalStoredToken = (side: 'buy' | 'sell', token?: TokenConfig) => { if (token) { - localStorage.setItem('swap-' + side, JSON.stringify({ - symbol: token.symbol, - name: token.name, - decimals: token.decimals, - faucetIdBech32: token.faucetIdBech32, - oracleId: token.oracleId, - })); + localStorage.setItem( + 'swap-' + side, + JSON.stringify({ + symbol: token.symbol, + name: token.name, + decimals: token.decimals, + faucetIdBech32: token.faucetIdBech32, + oracleId: token.oracleId, + }), + ); } }; diff --git a/src/pages/XykPoolDetail.tsx b/src/pages/XykPoolDetail.tsx new file mode 100644 index 0000000..82cd6a5 --- /dev/null +++ b/src/pages/XykPoolDetail.tsx @@ -0,0 +1,242 @@ +import AssetIcon from '@/components/AssetIcon'; +import { IlRiskCard } from '@/components/IlRiskCard'; +import { fullNumberBigintFormat, prettyBigintFormat } from '@/lib/format'; +import { PoolCompositionCard } from '@/components/PoolCompositionCard'; +import { PoolDetailHeader } from '@/components/PoolDetailHeader'; +import { PoolDetailLayout } from '@/components/PoolDetailLayout'; +import { PoolDetailStats } from '@/components/PoolDetailStats'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { PoolInfoCard } from '@/components/PoolInfoCard'; +import { RecentTransactionsCard } from '@/components/RecentTransactionsCard'; +import { XykPoolModal } from '@/components/XykPoolModal'; +import { useXykLpBalance } from '@/hooks/useXykLpBalance'; +import { useXykPool } from '@/hooks/useXykPool'; +import { useXykPoolNotes } from '@/hooks/useXykPoolNotes'; +import { getMockRecentTransactions } from '@/mocks/poolDetailMocks'; +import { ModalContext } from '@/providers/ModalContext'; +import { useCallback, useContext, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +const feeTierForSymbol = (_symbol: string) => '0.30%'; + +export default function XykPoolDetail() { + const { poolId } = useParams<{ poolId: string }>(); + const decodedPoolId = poolId ? decodeURIComponent(poolId) : undefined; + const modalContext = useContext(ModalContext); + const { data: poolData, isLoading: poolLoading, error: poolError } = useXykPool( + decodedPoolId, + ); + const { lpBalance, refetch: refetchLpBalance } = useXykLpBalance(decodedPoolId); + const { notes: poolNotes, isLoading: notesLoading, error: notesError } = useXykPoolNotes( + decodedPoolId, + poolData ?? null, + ); + const hasPosition = lpBalance > BigInt(0); + + const openXykModal = useCallback( + (mode: 'Deposit' | 'Withdraw') => { + if (!decodedPoolId) return; + modalContext.openModal( + refetchLpBalance()} + />, + ); + }, + [decodedPoolId, modalContext, refetchLpBalance], + ); + + const mockRecentTxs = useMemo(() => { + if (!poolData) return []; + return getMockRecentTransactions({ + seedKey: decodedPoolId ?? '', + baseSymbol: poolData.token0.symbol, + }); + }, [decodedPoolId, poolData]); + + if (poolLoading && !poolData) { + return ( + +

Loading pool…

+
+ ); + } + + if (poolError || !poolData) { + return ( + +

+ {poolError ? poolError.message : 'Pool not found.'} +

+
+ ); + } + + const pairLabel = `${poolData.token0.symbol} / ${poolData.token1.symbol}`; + const feeTier = feeTierForSymbol(poolData.token0.symbol); + const priceDisplay = + poolData.priceToken0InToken1 > 0 + ? `1 ${poolData.token0.symbol} = ${poolData.priceToken0InToken1.toFixed(6)} ${poolData.token1.symbol}` + : '—'; + const totalSupplyFormatted = fullNumberBigintFormat({ + value: poolData.totalSupply, + expo: 18, + }); + + const assetSymbol = (faucetIdBech32: string) => { + if (faucetIdBech32 === poolData.token0.faucetIdBech32) return poolData.token0.symbol; + if (faucetIdBech32 === poolData.token1.faucetIdBech32) return poolData.token1.symbol; + return null; + }; + const assetDecimals = (faucetIdBech32: string) => { + if (faucetIdBech32 === poolData.token0.faucetIdBech32) return poolData.token0.decimals; + if (faucetIdBech32 === poolData.token1.faucetIdBech32) return poolData.token1.decimals; + return 18; + }; + + return ( + + openXykModal('Deposit')} + onWithdraw={() => openXykModal('Withdraw')} + hasPosition={hasPosition} + headerIcons={ +
+ + + + + + +
+ } + /> + +
+
+ + + + {prettyBigintFormat({ + value: poolData.reserve0, + expo: poolData.token0.decimals, + })} + + ), + }, + { + label: `${poolData.token1.symbol} Reserve`, + value: ( + + + {prettyBigintFormat({ + value: poolData.reserve1, + expo: poolData.token1.decimals, + })} + + ), + }, + ]} + /> + +
+ +
+ + + Pool notes + + + {notesLoading ? ( +

Loading notes…

+ ) : notesError ? ( +

{notesError.message}

+ ) : poolNotes.length === 0 ? ( +

No notes issued by this pool yet.

+ ) : ( +
+ + + + + + + + + + {poolNotes.map((row) => ( + + + + + + ))} + +
Note IDAssetsImplied price
+ {row.noteId.slice(0, 10)}…{row.noteId.slice(-6)} + + + {row.assets.map((a) => { + const sym = assetSymbol(a.faucetIdBech32); + return ( + + + {prettyBigintFormat({ value: a.amount, expo: assetDecimals(a.faucetIdBech32) })} + {sym ? ` ${sym}` : ''} + + ); + })} + + + {row.impliedPrice != null + ? `1 ${poolData.token0.symbol} = ${row.impliedPrice.toFixed(6)} ${poolData.token1.symbol}` + : '—'} +
+
+ )} +
+
+ + { + /* + + */ + } +
+
+
+ ); +} From 4118ec741664d765eff38f2dfc6f59a1abf1bf75 Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 12 Mar 2026 02:16:14 +0100 Subject: [PATCH 18/49] Debug withdraw --- src/components/PoolModal.tsx | 35 +- src/components/XykPoolModal.tsx | 781 ++++++++++++++++++++++++++++---- src/hooks/useLaunchpad.ts | 56 ++- src/hooks/useTokens.ts | 58 +++ src/hooks/useXykLpBalance.ts | 6 +- src/hooks/useXykPool.tsx | 15 +- src/hooks/useXykWithdraw.ts | 79 ++++ src/lib/XykDepositNote.ts | 4 +- src/lib/XykWithdrawNote.ts | 21 +- src/lib/format.ts | 8 + src/lib/xykMath.ts | 86 ++++ src/pages/Launchpad.tsx | 94 +++- src/pages/XykPoolDetail.tsx | 147 +++--- 13 files changed, 1170 insertions(+), 220 deletions(-) create mode 100644 src/hooks/useTokens.ts create mode 100644 src/hooks/useXykWithdraw.ts create mode 100644 src/lib/xykMath.ts diff --git a/src/components/PoolModal.tsx b/src/components/PoolModal.tsx index b34267f..0e85ea2 100644 --- a/src/components/PoolModal.tsx +++ b/src/components/PoolModal.tsx @@ -2,7 +2,7 @@ import { useDeposit } from '@/hooks/useDeposit'; import { usePoolsBalances } from '@/hooks/usePoolsBalances'; import { useWithdraw } from '@/hooks/useWithdraw'; import { DEFAULT_SLIPPAGE } from '@/lib/config'; -import { formatTokenAmount, formatUsd } from '@/lib/format'; +import { formatTokenAmount, formatTokenAmountForInput, formatUsd } from '@/lib/format'; import { useOraclePrices } from '@/providers/OracleContext'; import { ZoroContext } from '@/providers/ZoroContext'; import type { TokenConfig } from '@/providers/ZoroProvider'; @@ -167,7 +167,7 @@ export default function PoolModal({ setRawValue(newValue); setInputError(undefined); setInputValue( - (formatTokenAmount({ value: newValue, expo: decimals }) ?? '').toString(), + formatTokenAmountForInput({ value: newValue, expo: decimals }), ); if (mode === 'Deposit') setDepositPct(percentage); if (mode === 'Withdraw') setWithdrawPct(percentage); @@ -177,15 +177,16 @@ export default function PoolModal({ const onInputChange = useCallback( (val: string) => { - setInputValue(val); - if (val === '') { + const s = typeof val === 'string' ? val : ''; + setInputValue(s); + if (s === '') { setInputError(undefined); setRawValue(BigInt(0)); if (mode === 'Deposit') setDepositPct(0); if (mode === 'Withdraw') setWithdrawPct(0); return; } - const parsed = parseUnits(val, decimals); + const parsed = parseUnits(s, decimals); const validationError = validateValue(parsed, balance); if (validationError) setInputError(validationError); else { @@ -202,26 +203,6 @@ export default function PoolModal({ [decimals, balance, mode], ); - useEffect(() => { - if (mode === 'Deposit' && balance > BigInt(0)) { - const newValue = (BigInt(depositPct) * balance) / BigInt(100); - setRawValue(newValue); - setInputValue( - (formatTokenAmount({ value: newValue, expo: decimals }) ?? '').toString(), - ); - } - }, [depositPct, mode, balance, decimals]); - - useEffect(() => { - if (mode === 'Withdraw' && balance > BigInt(0)) { - const newValue = (BigInt(withdrawPct) * balance) / BigInt(100); - setRawValue(newValue); - setInputValue( - (formatTokenAmount({ value: newValue, expo: decimals }) ?? '').toString(), - ); - } - }, [withdrawPct, mode, balance, decimals]); - const handleClose = useCallback(() => modalContext.closeModal(), [modalContext]); const poolLabel = pool.name || (isHfAmm ? `${pool.symbol}` : `${pool.symbol} / USDC`); @@ -437,7 +418,7 @@ export default function PoolModal({

Amount

onInputChange(e.target.value)} @@ -551,7 +532,7 @@ export default function PoolModal({

Amount

onInputChange(e.target.value)} diff --git a/src/components/XykPoolModal.tsx b/src/components/XykPoolModal.tsx index e560cef..7d4e4ac 100644 --- a/src/components/XykPoolModal.tsx +++ b/src/components/XykPoolModal.tsx @@ -1,20 +1,37 @@ import AssetIcon from '@/components/AssetIcon'; +import Slippage from '@/components/Slippage'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { ModalContext } from '@/providers/ModalContext'; -import { formatTokenAmount } from '@/lib/format'; +import { useTokens } from '@/hooks/useTokens'; import { useXykDeposit } from '@/hooks/useXykDeposit'; import { useXykLpBalance } from '@/hooks/useXykLpBalance'; import { useXykPool } from '@/hooks/useXykPool'; -import { X } from 'lucide-react'; -import { useCallback, useContext, useState } from 'react'; +import type { XykTokenInfo } from '@/hooks/useXykPool'; +import { useXykWithdraw } from '@/hooks/useXykWithdraw'; +import { DEFAULT_SLIPPAGE } from '@/lib/config'; +import { formatTokenAmount, formatTokenAmountForInput } from '@/lib/format'; +import { computeExpectedLp, computeExpectedWithdraw } from '@/lib/xykMath'; +import { ModalContext } from '@/providers/ModalContext'; +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { AlertTriangle, Info, Loader, X } from 'lucide-react'; +import { useCallback, useContext, useMemo, useState } from 'react'; import { parseUnits } from 'viem'; +import { useBalance } from '../hooks/useBalance'; + +/** LP shares have no decimals (raw integer). */ +const LP_EXPO = 0; +const PERCENTAGES = [25, 50, 75, 100] as const; + +const validateValue = (val: bigint, max: bigint) => + val > max ? 'Amount too large' : val <= 0n ? 'Invalid value' : undefined; + +export type LpActionType = 'Deposit' | 'Withdraw'; export interface XykPoolModalProps { poolId: string; - onSuccess?: (noteId: string) => void; + onSuccess?: () => void; onClose?: () => void; - initialMode?: 'Deposit' | 'Withdraw'; + initialMode?: LpActionType; } export function XykPoolModal({ @@ -26,27 +43,347 @@ export function XykPoolModal({ const modalContext = useContext(ModalContext); const { data: poolData, isLoading: poolLoading } = useXykPool(poolId); const { lpBalance, refetch: refetchLpBalance } = useXykLpBalance(poolId); - const { deposit, isLoading: isDepositLoading } = useXykDeposit(poolId); + const faucetIds = useMemo( + () => + poolData + ? [ + poolData.token0.faucetIdBech32, + poolData.token1.faucetIdBech32, + ] + : [], + [poolData], + ); + const { tokens: tokensMetadata } = useTokens(faucetIds); + + const { + deposit, + isLoading: isDepositLoading, + error: depositError, + } = useXykDeposit(poolId); + const { + withdraw, + isLoading: isWithdrawLoading, + error: withdrawError, + } = useXykWithdraw(poolId); + + const xykTokenToConfig = useCallback((t: XykTokenInfo): TokenConfig => ({ + symbol: t.symbol, + name: t.name ?? t.symbol, + decimals: t.decimals, + faucetId: t.faucetId, + faucetIdBech32: t.faucetIdBech32, + oracleId: '', + }), []); - const [mode] = useState<'Deposit' | 'Withdraw'>(initialMode); + const token0Config = useMemo( + () => + poolData + ? (tokensMetadata[poolData.token0.faucetIdBech32] + ?? xykTokenToConfig(poolData.token0)) + : undefined, + [poolData, tokensMetadata, xykTokenToConfig], + ); + const token1Config = useMemo( + () => + poolData + ? (tokensMetadata[poolData.token1.faucetIdBech32] + ?? xykTokenToConfig(poolData.token1)) + : undefined, + [poolData, tokensMetadata, xykTokenToConfig], + ); + + const { balance: balance0 } = useBalance({ token: token0Config }); + const { balance: balance1 } = useBalance({ token: token1Config }); + + const [mode, setMode] = useState(initialMode); + const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE); const [amount0Str, setAmount0Str] = useState(''); const [amount1Str, setAmount1Str] = useState(''); + const [lpAmountStr, setLpAmountStr] = useState(''); const [inputError, setInputError] = useState(); + const [depositPct, setDepositPct] = useState(100); + const [withdrawPct, setWithdrawPct] = useState(100); + + const clearForm = useCallback(() => { + setAmount0Str(''); + setAmount1Str(''); + setLpAmountStr(''); + setDepositPct(100); + setWithdrawPct(100); + setInputError(undefined); + }, []); const handleClose = useCallback(() => { modalContext.closeModal(); onClose?.(); }, [modalContext, onClose]); - const handleDeposit = useCallback(async () => { - if (!poolData) return; - let amount0: bigint; - let amount1: bigint; + const amount0 = useMemo(() => { + const s = typeof amount0Str === 'string' ? amount0Str.trim() : ''; + if (!poolData || !s) return 0n; + try { + return parseUnits(s, poolData.token0.decimals); + } catch { + return 0n; + } + }, [poolData, amount0Str]); + + const amount1 = useMemo(() => { + const s = typeof amount1Str === 'string' ? amount1Str.trim() : ''; + if (!poolData || !s) return 0n; + try { + return parseUnits(s, poolData.token1.decimals); + } catch { + return 0n; + } + }, [poolData, amount1Str]); + + const lpAmount = useMemo(() => { + const s = typeof lpAmountStr === 'string' ? lpAmountStr.trim() : ''; + if (!s) return 0n; try { - amount0 = parseUnits(amount0Str || '0', poolData.token0.decimals); - amount1 = parseUnits(amount1Str || '0', poolData.token1.decimals); + return parseUnits(s, LP_EXPO); } catch { - setInputError('Invalid amounts'); + return 0n; + } + }, [lpAmountStr]); + + const expectedLp = useMemo(() => { + if (!poolData || (amount0 === 0n && amount1 === 0n)) return 0n; + return computeExpectedLp( + amount0, + amount1, + poolData.reserve0, + poolData.reserve1, + poolData.totalSupply, + ); + }, [poolData, amount0, amount1]); + + const minAmountOutDeposit = useMemo(() => { + if (expectedLp === 0n) return 1n; + const slippageMultiplier = BigInt(Math.round((100 - slippage) * 1e6)); + const min = (expectedLp * slippageMultiplier) / BigInt(1e8); + return min > 0n ? min : 1n; + }, [expectedLp, slippage]); + + const [expectedWithdraw0, expectedWithdraw1] = useMemo(() => { + if (!poolData || lpAmount === 0n || poolData.totalSupply === 0n) { + return [0n, 0n]; + } + return computeExpectedWithdraw( + poolData.totalSupply, + lpAmount, + poolData.reserve0, + poolData.reserve1, + ); + }, [poolData, lpAmount]); + + const minAmountOutWithdraw0 = useMemo(() => { + if (expectedWithdraw0 === 0n) return 1n; + const slippageMultiplier = BigInt(Math.round((100 - slippage) * 1e6)); + const min = (expectedWithdraw0 * slippageMultiplier) / BigInt(1e8); + return min > 0n ? min : 1n; + }, [expectedWithdraw0, slippage]); + + const minAmountOutWithdraw1 = useMemo(() => { + if (expectedWithdraw1 === 0n) return 1n; + const slippageMultiplier = BigInt(Math.round((100 - slippage) * 1e6)); + const min = (expectedWithdraw1 * slippageMultiplier) / BigInt(1e8); + return min > 0n ? min : 1n; + }, [expectedWithdraw1, slippage]); + + const poolSharePct = useMemo(() => { + if (!poolData || expectedLp === 0n) return null; + const newTotalLp = poolData.totalSupply + expectedLp; + if (newTotalLp === 0n) return null; + return (Number(expectedLp) / Number(newTotalLp)) * 100; + }, [poolData, expectedLp]); + + const poolShareDisplay = poolSharePct != null + ? poolSharePct < 0.01 + ? `${poolSharePct.toFixed(6)}%` + : `${poolSharePct.toFixed(2)}%` + : '—'; + + const withdrawPoolSharePct = useMemo(() => { + if (!poolData || lpAmount === 0n) return null; + const totalAfter = poolData.totalSupply - lpAmount; + if (totalAfter <= 0n) return null; + const userAfter = (lpBalance ?? 0n) >= lpAmount ? (lpBalance ?? 0n) - lpAmount : 0n; + const pct = (Number(userAfter) / Number(totalAfter)) * 100; + return Math.min(100, Math.max(0, pct)); + }, [poolData, lpAmount, lpBalance]); + + const withdrawPoolShareDisplay = withdrawPoolSharePct != null + ? withdrawPoolSharePct < 0.01 + ? `${withdrawPoolSharePct.toFixed(6)}%` + : `${withdrawPoolSharePct.toFixed(2)}%` + : '—'; + + const maxDepositA0 = useMemo(() => { + if (!poolData) return 0n; + const b0 = balance0 ?? 0n; + const b1 = balance1 ?? 0n; + const r0 = poolData.reserve0; + const r1 = poolData.reserve1; + if (r1 === 0n) return b0; + const fromB1 = (b1 * r0) / r1; + return b0 < fromB1 ? b0 : fromB1; + }, [poolData, balance0, balance1]); + + const setDepositPercentage = useCallback( + (percentage: number) => { + if (!poolData || maxDepositA0 === 0n) return; + const r0 = poolData.reserve0; + const r1 = poolData.reserve1; + const a0 = (maxDepositA0 * BigInt(percentage)) / 100n; + const a1 = (a0 * r1) / r0; + setAmount0Str( + formatTokenAmountForInput({ + value: a0, + expo: poolData.token0.decimals, + }), + ); + setAmount1Str( + formatTokenAmountForInput({ + value: a1, + expo: poolData.token1.decimals, + }), + ); + setDepositPct(percentage); + setInputError(undefined); + }, + [poolData, maxDepositA0], + ); + + const onDepositAmount0Change = useCallback( + (val: string) => { + const s = typeof val === 'string' ? val : ''; + setAmount0Str(s); + if (s === '') { + setAmount1Str(''); + setDepositPct(0); + setInputError(undefined); + return; + } + try { + const parsed = parseUnits(s, poolData?.token0.decimals ?? 18); + const b0 = balance0 ?? 0n; + const err = validateValue(parsed, b0); + if (err) { + setInputError(err); + return; + } + setInputError(undefined); + if (maxDepositA0 > 0n) { + const pct = Number((parsed * 100n) / maxDepositA0); + setDepositPct(Math.min(100, Math.max(0, pct))); + } + if (poolData && poolData.reserve0 > 0n && parsed > 0n) { + const a1 = (parsed * poolData.reserve1) / poolData.reserve0; + setAmount1Str( + formatTokenAmountForInput({ + value: a1, + expo: poolData.token1.decimals, + }), + ); + } + } catch { + setInputError('Invalid value'); + } + }, + [poolData, balance0, maxDepositA0], + ); + + const onDepositAmount1Change = useCallback( + (val: string) => { + const s = typeof val === 'string' ? val : ''; + setAmount1Str(s); + if (s === '') { + setAmount0Str(''); + setDepositPct(0); + setInputError(undefined); + return; + } + try { + const parsed = parseUnits(s, poolData?.token1.decimals ?? 18); + const b1 = balance1 ?? 0n; + const err = validateValue(parsed, b1); + if (err) { + setInputError(err); + return; + } + setInputError(undefined); + if (poolData && poolData.reserve1 > 0n) { + const maxA1 = (maxDepositA0 * poolData.reserve1) / poolData.reserve0; + if (maxA1 > 0n) { + const pct = Number((parsed * 100n) / maxA1); + setDepositPct(Math.min(100, Math.max(0, pct))); + } + } + if (poolData && poolData.reserve1 > 0n && parsed > 0n) { + const a0 = (parsed * poolData.reserve0) / poolData.reserve1; + setAmount0Str( + formatTokenAmountForInput({ + value: a0, + expo: poolData.token0.decimals, + }), + ); + } + } catch { + setInputError('Invalid value'); + } + }, + [poolData, balance1, maxDepositA0], + ); + + const setWithdrawPercentage = useCallback( + (percentage: number) => { + const bal = lpBalance ?? 0n; + const newValue = (bal * BigInt(percentage)) / 100n; + setLpAmountStr( + formatTokenAmountForInput({ value: newValue, expo: LP_EXPO }), + ); + setWithdrawPct(percentage); + setInputError(undefined); + }, + [lpBalance], + ); + + const onWithdrawInputChange = useCallback( + (val: string) => { + const s = typeof val === 'string' ? val : ''; + setLpAmountStr(s); + if (s === '') { + setWithdrawPct(0); + setInputError(undefined); + return; + } + try { + const parsed = parseUnits(s, LP_EXPO); + const bal = lpBalance ?? 0n; + const err = validateValue(parsed, bal); + if (err) setInputError(err); + else { + setInputError(undefined); + if (bal > 0n) { + const pct = Number((parsed * 100n) / bal); + setWithdrawPct(Math.min(100, Math.max(0, pct))); + } + } + } catch { + setInputError('Invalid value'); + } + }, + [lpBalance], + ); + + const writeDeposit = useCallback(async () => { + if (!poolData) return; + const b0 = balance0 ?? 0n; + const b1 = balance1 ?? 0n; + if (amount0 > b0 || amount1 > b1) { + setInputError('Insufficient balance'); return; } if (amount0 <= 0n && amount1 <= 0n) { @@ -55,111 +392,387 @@ export function XykPoolModal({ } setInputError(undefined); const result = await deposit(amount0, amount1); - refetchLpBalance(); - handleClose(); - if (result?.noteId && onSuccess) onSuccess(result.noteId); + console.log(result); + if (result) { + clearForm(); + refetchLpBalance(); + onSuccess?.(); + handleClose(); + } }, [ poolData, - amount0Str, - amount1Str, + amount0, + amount1, + balance0, + balance1, deposit, + clearForm, refetchLpBalance, + onSuccess, handleClose, + ]); + + const writeWithdraw = useCallback(async () => { + if (!poolData) return; + const bal = lpBalance ?? 0n; + if (lpAmount > bal) { + setInputError('Insufficient LP balance'); + return; + } + if (lpAmount <= 0n) { + setInputError('Enter amount'); + return; + } + setInputError(undefined); + const result = await withdraw(lpAmount); + if (result) { + clearForm(); + refetchLpBalance(); + onSuccess?.(); + handleClose(); + } + }, [ + poolData, + lpAmount, + lpBalance, + withdraw, + clearForm, + refetchLpBalance, onSuccess, + handleClose, ]); if (poolLoading || !poolData) { return ( -
+

Loading pool…

); } const pairLabel = `${poolData.token0.symbol} / ${poolData.token1.symbol}`; + const expectedLpFormatted = formatTokenAmount({ + value: expectedLp, + expo: LP_EXPO, + }); + const minLpFormatted = formatTokenAmount({ + value: minAmountOutDeposit, + expo: LP_EXPO, + }) ?? '0'; + const minWithdraw0Formatted = formatTokenAmount({ + value: minAmountOutWithdraw0, + expo: poolData.token0.decimals, + }) ?? '0'; + const minWithdraw1Formatted = formatTokenAmount({ + value: minAmountOutWithdraw1, + expo: poolData.token1.decimals, + }) ?? '0'; + const withdrawReceiveFormatted = formatTokenAmount({ value: lpAmount, expo: LP_EXPO }) + ?? '0'; return ( -
-
-

{pairLabel}

+
+
+
+
+ + + + + + +
+ + {mode === 'Withdraw' ? `Withdraw from ${pairLabel}` : pairLabel} + +
+ +
+ +
+
- {mode === 'Deposit' ? ( -
-
- -
- setAmount0Str(e.target.value)} - className='flex-1' + {mode === 'Deposit' && ( + <> +
+

+ Deposit amounts +

+ +
+
+
+

+ {poolData.token0.symbol} +

+
+ onDepositAmount0Change(e.target.value)} + /> +
+ + Balance: {formatTokenAmount({ + value: balance0 ?? 0n, + expo: poolData.token0.decimals, + })} {poolData.token0.symbol} + + + + +
+
+
+
+

+ {poolData.token1.symbol} +

+
+ onDepositAmount1Change(e.target.value)} + /> +
+ + Balance: {formatTokenAmount({ + value: balance1 ?? 0n, + expo: poolData.token1.decimals, + })} {poolData.token1.symbol} + + + + +
+
+
+
+ {PERCENTAGES.map((n) => ( + + ))} +
+
+ Deposit percentage + {depositPct}% +
+
+
-
- - - {poolData.token0.symbol} - +
+
+
+

+ You receive (min) +

+
+
+ LP
+ {minLpFormatted} +
+ {expectedLpFormatted != null + && expectedLpFormatted !== minLpFormatted && ( +

+ Expected: {expectedLpFormatted} +

+ )} +
+ {inputError &&

{inputError}

} +
+ +
+ Pool Share + {poolShareDisplay}
-
- -
- setAmount1Str(e.target.value)} - className='flex-1' + + + )} + + {mode === 'Withdraw' && ( + <> +
+

+ Withdraw amount +

+ +
+
+
+

Amount

+
+ onWithdrawInputChange(e.target.value)} + /> +
+ + Balance: {formatTokenAmount({ + value: lpBalance ?? 0n, + expo: LP_EXPO, + })} LP + + + + +
+
+
+ {PERCENTAGES.map((n) => ( + + ))} +
+
+
+ Withdraw percentage + {withdrawPct}% +
+
+
-
+
+
+
+

+ You receive (min) +

+

+ LP: {withdrawReceiveFormatted} +

+
+
+ + {poolData.token0.symbol} +
+ + {formatTokenAmount({ + value: expectedWithdraw0, + expo: poolData.token0.decimals, + })} (min: {minWithdraw0Formatted}) + +
+
+
- - {poolData.token1.symbol} - + {poolData.token1.symbol}
+ + {formatTokenAmount({ + value: expectedWithdraw1, + expo: poolData.token1.decimals, + })} (min: {minWithdraw1Formatted}) + +
+
+
+ +
+ + Remaining pool share + + {withdrawPoolShareDisplay}
- {inputError && ( -

{inputError}

- )} +
+ +
+

+ Impermanent Loss Notice +

+

+ Withdrawing now realizes any impermanent loss. Your position may have + experienced IL since deposit. If you deposited at a different price ratio, + you may receive fewer tokens than expected. +

+
+
+ {inputError &&

{inputError}

} -
- ) : ( -
-

- Your LP balance:{' '} - {formatTokenAmount({ - value: lpBalance, - expo: 18, - }) ?? '0'} -

-

- Withdraw liquidity — coming soon. -

-
+ + )} + + {(depositError || withdrawError) && ( +

+ {depositError ?? withdrawError} +

)}
); diff --git a/src/hooks/useLaunchpad.ts b/src/hooks/useLaunchpad.ts index 8852106..e771fb1 100644 --- a/src/hooks/useLaunchpad.ts +++ b/src/hooks/useLaunchpad.ts @@ -23,32 +23,50 @@ export function getMidenscanAccountUrl(accountBech32: string): string { return `${MIDENSCAN_BASE}/account/${accountBech32}`; } +export const LAUNCH_STEPS = [ + 'Creating faucet', + 'Minting initial supply', + 'Finalizing', +] as const; + +export type LaunchStepIndex = 0 | 1 | 2; + const useLaunchpad = () => { const [error, setError] = useState(''); const { accountId, createFaucet, mintFromFaucet } = useContext(ZoroContext); const clearError = useCallback(() => setError(''), []); - const launchToken = useCallback(async (params: FaucetParams): Promise => { - setError(''); - try { - if (!accountId) { - throw new Error('Connect your wallet to use the launchpad'); - } - const faucet = await createFaucet(params); - if (!faucet) { - throw new Error('Faucet creation failed'); + const launchToken = useCallback( + async ( + params: FaucetParams, + options?: { onProgress?: (step: LaunchStepIndex) => void }, + ): Promise => { + setError(''); + const onProgress = options?.onProgress; + try { + if (!accountId) { + throw new Error('Connect your wallet to use the launchpad'); + } + onProgress?.(0); + const faucet = await createFaucet(params); + if (!faucet) { + throw new Error('Faucet creation failed'); + } + onProgress?.(1); + const txId = await mintFromFaucet(faucet.id(), accountId, params.initialSupply); + onProgress?.(2); + const faucetIdBech32 = accountIdToBech32(faucet.id()); + return { txId, faucetIdBech32 }; + } catch (e) { + const message = e instanceof Error ? e.message : typeof e === 'string' ? e : 'Launch failed. Check the console for details.'; + setError(message); + console.error('Launchpad error:', e); + return undefined; } - const faucetIdBech32 = accountIdToBech32(faucet.id()); - const txId = await mintFromFaucet(faucet.id(), accountId, params.initialSupply); - return { txId, faucetIdBech32 }; - } catch (e) { - const message = e instanceof Error ? e.message : typeof e === 'string' ? e : 'Launch failed. Check the console for details.'; - setError(message); - console.error('Launchpad error:', e); - return undefined; - } - }, [accountId, createFaucet, mintFromFaucet]); + }, + [accountId, createFaucet, mintFromFaucet], + ); const value = useMemo(() => ({ launchToken, diff --git a/src/hooks/useTokens.ts b/src/hooks/useTokens.ts new file mode 100644 index 0000000..d9215b8 --- /dev/null +++ b/src/hooks/useTokens.ts @@ -0,0 +1,58 @@ +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { accountIdToBech32, bech32ToAccountId } from '@/lib/utils'; +import { ZoroContext } from '@/providers/ZoroContext'; +import { BasicFungibleFaucetComponent } from '@miden-sdk/miden-sdk'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; + +/** + * Fetches token metadata from the network for the given faucet IDs. + * Converts each faucet account to TokenConfig the same way as getAvailableTokens in ZoroProvider. + * Returns tokens keyed by faucetIdBech32. + */ +export function useTokens(faucetIds: string[] | undefined) { + const { rpcClient } = useContext(ZoroContext); + const [tokens, setTokens] = useState>({}); + const [loading, setLoading] = useState(false); + + const refresh = useCallback(async () => { + if (!rpcClient || !faucetIds?.length) { + setTokens({}); + setLoading(false); + return; + } + setLoading(true); + try { + const result: Record = {}; + for (const bech32 of faucetIds) { + const faucetId = bech32ToAccountId(bech32); + if (!faucetId) continue; + try { + const details = await rpcClient.getAccountDetails(faucetId); + const account = details.account(); + if (!account) continue; + const faucet = BasicFungibleFaucetComponent.fromAccount(account); + const symbol = faucet.symbol().toString(); + result[accountIdToBech32(faucetId)] = { + symbol, + decimals: faucet.decimals(), + name: symbol, + faucetId, + faucetIdBech32: accountIdToBech32(faucetId), + oracleId: '0x', + }; + } catch { + // skip failed faucet + } + } + setTokens(result); + } finally { + setLoading(false); + } + }, [rpcClient, faucetIds]); + + useEffect(() => { + refresh(); + }, [refresh]); + + return useMemo(() => ({ tokens, loading, refresh }), [tokens, loading, refresh]); +} diff --git a/src/hooks/useXykLpBalance.ts b/src/hooks/useXykLpBalance.ts index 21e08f8..0e3d608 100644 --- a/src/hooks/useXykLpBalance.ts +++ b/src/hooks/useXykLpBalance.ts @@ -1,4 +1,4 @@ -import { bech32ToAccountId, accountIdToBech32 } from '@/lib/utils'; +import { accountIdToBech32, bech32ToAccountId } from '@/lib/utils'; import { ZoroContext } from '@/providers/ZoroContext'; import { Felt, Word } from '@miden-sdk/miden-sdk'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; @@ -30,10 +30,10 @@ export function useXykLpBalance(poolId: string | undefined) { ); const storage = fetched.account()?.storage(); const key = Word.newFromFelts([ - new Felt(accountId.prefix().asInt()), - new Felt(accountId.suffix().asInt()), new Felt(BigInt(0)), new Felt(BigInt(0)), + new Felt(accountId.suffix().asInt()), + new Felt(accountId.prefix().asInt()), ]); const value = storage?.getMapItem( 'zoro::lp_local::user_deposits_mapping', diff --git a/src/hooks/useXykPool.tsx b/src/hooks/useXykPool.tsx index 376cdc7..3897a40 100644 --- a/src/hooks/useXykPool.tsx +++ b/src/hooks/useXykPool.tsx @@ -26,7 +26,6 @@ export interface XykPoolData { totalSupply: bigint; reserve0: bigint; reserve1: bigint; - /** 1 token0 = priceToken0InToken1 token1 (human units). */ priceToken0InToken1: number; } @@ -90,8 +89,8 @@ export function useXykPool(poolId: string | undefined) { ); const totalSupplyWord = storage.getItem('zoro::lp_local::total_supply'); - const totalSupply = totalSupplyWord?.toFelts()?.[0]?.asInt() ?? BigInt(0); - console.log(totalSupply); + const totalSupplyFelts = totalSupplyWord?.toFelts() ?? []; + const totalSupply = totalSupplyFelts[0]?.asInt() ?? BigInt(0); const reserveWord = storage.getItem('zoro::lp_local::reserve'); const reserveFelts = reserveWord?.toFelts() ?? []; @@ -130,10 +129,10 @@ export function useXykPool(poolId: string | undefined) { fetchFaucetInfo(token1Id), ]); - const priceToken0InToken1 = - reserve1 > 0n - ? Number(reserve1) / 10 ** token1.decimals / (Number(reserve0) / 10 ** token0.decimals) - : 0; + const priceToken0InToken1 = reserve1 > 0n + ? Number(reserve1) / 10 ** token1.decimals + / (Number(reserve0) / 10 ** token0.decimals) + : 0; setData({ token0, @@ -149,7 +148,7 @@ export function useXykPool(poolId: string | undefined) { } finally { setIsLoading(false); } - }, [poolId, rpcClient, accountId]); + }, [poolId, rpcClient]); useEffect(() => { refetch(); diff --git a/src/hooks/useXykWithdraw.ts b/src/hooks/useXykWithdraw.ts new file mode 100644 index 0000000..1bad9f7 --- /dev/null +++ b/src/hooks/useXykWithdraw.ts @@ -0,0 +1,79 @@ +import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; +import { useXykPool } from '@/hooks/useXykPool'; +import { clientMutex } from '@/lib/clientMutex'; +import { bech32ToAccountId } from '@/lib/utils'; +import { compileXykWithdrawTransaction } from '@/lib/XykWithdrawNote'; +import { ZoroContext } from '@/providers/ZoroContext'; +import { TransactionType } from '@demox-labs/miden-wallet-adapter'; +import { useCallback, useContext, useMemo, useState } from 'react'; +import { toast } from 'react-toastify'; + +export function useXykWithdraw(poolId: string | undefined) { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + const [txId, setTxId] = useState(); + const [noteId, setNoteId] = useState(); + const { requestTransaction } = useUnifiedWallet(); + const { client, accountId, syncState } = useContext(ZoroContext); + const { data: poolData } = useXykPool(poolId); + + const withdraw = useCallback( + async ( + lpAmount: bigint, + ): Promise<{ noteId: string; txId: string | undefined } | undefined> => { + if ( + !poolId + || !poolData + || !client + || !accountId + || !requestTransaction + ) { + return undefined; + } + const poolAccountId = bech32ToAccountId(poolId); + if (!poolAccountId) return undefined; + setError(''); + setIsLoading(true); + try { + await syncState(); + const { tx, noteId: nid } = await clientMutex.runExclusive(() => + compileXykWithdrawTransaction({ + poolAccountId, + userAccountId: accountId, + token0: poolData.token0.faucetId, + token1: poolData.token1.faucetId, + lpAmount, + client, + }) + ); + const txIdResult = await requestTransaction({ + type: TransactionType.Custom, + payload: tx, + }); + await syncState(); + setNoteId(nid); + setTxId(txIdResult); + return { noteId: nid, txId: txIdResult }; + } catch (err) { + console.error(err); + toast.error(`Error withdrawing liquidity: ${err}`); + } finally { + setIsLoading(false); + } + return undefined; + }, + [ + poolId, + poolData, + client, + accountId, + requestTransaction, + syncState, + ], + ); + + return useMemo( + () => ({ withdraw, isLoading, error, txId, noteId }), + [withdraw, isLoading, error, txId, noteId], + ); +} diff --git a/src/lib/XykDepositNote.ts b/src/lib/XykDepositNote.ts index 5293e93..1dd0a12 100644 --- a/src/lib/XykDepositNote.ts +++ b/src/lib/XykDepositNote.ts @@ -18,7 +18,7 @@ import { WebClient, } from '@miden-sdk/miden-sdk'; -import DEPOSIT_SCRIPT from '@/masm/notes/xyk_deposit.masm?raw'; +import SCRIPT from '@/masm/notes/xyk_deposit.masm?raw'; import { build_lp_local_lib } from './DeployXykPool'; import { accountIdToBech32, generateRandomSerialNumber } from './utils'; @@ -50,7 +50,7 @@ export async function compileXykDepositTransaction({ const builder = client.createCodeBuilder(); builder.linkStaticLibrary(lp_local_lib); const script = builder.compileNoteScript( - DEPOSIT_SCRIPT, + SCRIPT, ); const noteTag = NoteTag.withAccountTarget(poolAccountId); diff --git a/src/lib/XykWithdrawNote.ts b/src/lib/XykWithdrawNote.ts index 56da5ec..9671f92 100644 --- a/src/lib/XykWithdrawNote.ts +++ b/src/lib/XykWithdrawNote.ts @@ -8,10 +8,14 @@ import { Note, NoteAssets, NoteAttachment, + NoteDetails, + NoteDetailsAndTag, + NoteDetailsAndTagArray, NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, + NoteRecipientArray, NoteTag, NoteType, OutputNote, @@ -53,12 +57,10 @@ export async function compileXykWithdrawTransaction({ ); const noteTag = NoteTag.withAccountTarget(poolAccountId); - const attachment = NoteAttachment.newNetworkAccountTarget( poolAccountId, NoteExecutionHint.always(), ); - const metadata = new NoteMetadata( userAccountId, NoteType.Public, @@ -67,18 +69,18 @@ export async function compileXykWithdrawTransaction({ const returnNoteTag = NoteTag.withAccountTarget(userAccountId); const returnNoteType = NoteType.Public; - const noteAssets = new NoteAssets([ + const returnNoteAssets = new NoteAssets([ new FungibleAsset(token0, BigInt(1)), new FungibleAsset(token1, BigInt(1)), ]); const returnNote = Note.createP2IDNote( poolAccountId, userAccountId, - noteAssets, + returnNoteAssets, returnNoteType, new NoteAttachment(), ); - const returnRecipientDigest = returnNote.recipient().digest().toFelts(); + const returnNoteRecipientDigest = returnNote.recipient().digest().toFelts(); const inputs = new NoteInputs( new FeltArray([ @@ -90,13 +92,14 @@ export async function compileXykWithdrawTransaction({ new Felt(BigInt(returnNoteType)), new Felt(BigInt(0)), new Felt(BigInt(0)), - returnRecipientDigest[0], - returnRecipientDigest[1], - returnRecipientDigest[2], - returnRecipientDigest[3], + returnNoteRecipientDigest[0], + returnNoteRecipientDigest[1], + returnNoteRecipientDigest[2], + returnNoteRecipientDigest[3], ]), ); + const noteAssets = new NoteAssets([]); const note = new Note( noteAssets, metadata, diff --git a/src/lib/format.ts b/src/lib/format.ts index c849d93..19b80e6 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -17,6 +17,14 @@ export const formatTokenAmount = ( if (value == null) return undefined; return roundDown(formatUnits(BigInt(value), expo), digits); }; + +/** Always returns a string for use in controlled inputs. formatTokenAmount may return number. */ +export const formatTokenAmountForInput = ( + opts: { value?: bigint | null; expo: number; digits?: number }, +): string => { + const v = formatTokenAmount(opts); + return v != null ? String(v) : ''; +}; export const prettyBigintFormat = ( { value, expo }: { value?: bigint; expo: number }, ) => { diff --git a/src/lib/xykMath.ts b/src/lib/xykMath.ts new file mode 100644 index 0000000..a65c24e --- /dev/null +++ b/src/lib/xykMath.ts @@ -0,0 +1,86 @@ +/** + * XYK pool math matching on-chain logic (constant product, 30BP fee). + * All amounts are in token/LP raw units (e.g. 10^decimals). + */ + +/** Integer square root (floor). */ +export function isqrt(n: bigint): bigint { + if (n < 0n) throw new Error('isqrt: negative'); + if (n < 2n) return n; + let x = n; + let y = (x + 1n) / 2n; + while (y < x) { + x = y; + y = (x + n / x) / 2n; + } + return x; +} + +/** + * Expected LP tokens received for depositing amount0 and amount1. + * When total_lp === 0 (first deposit): isqrt(amount0 * amount1) - 100. + * Otherwise: min(amount0 * total_lp / reserve0, amount1 * total_lp / reserve1). + */ +export function computeExpectedLp( + amount0: bigint, + amount1: bigint, + reserve0: bigint, + reserve1: bigint, + totalLp: bigint, +): bigint { + if (totalLp === 0n) { + const product = amount0 * amount1; + const root = isqrt(product); + return root > 100n ? root - 100n : 0n; + } + const lp0 = (amount0 * totalLp) / reserve0; + const lp1 = (amount1 * totalLp) / reserve1; + return lp0 < lp1 ? lp0 : lp1; +} + +/** + * Expected token amounts received when burning lpAmount. + * amount0 = lp_amount * reserve_0 / total_supply, amount1 = lp_amount * reserve_1 / total_supply. + */ +export function computeExpectedWithdraw( + totalSupply: bigint, + lpAmount: bigint, + reserve0: bigint, + reserve1: bigint, +): [bigint, bigint] { + if (totalSupply === 0n) return [0n, 0n]; + const amount0 = (lpAmount * reserve0) / totalSupply; + const amount1 = (lpAmount * reserve1) / totalSupply; + return [amount0, amount1]; +} + +/** + * Exact tokens for tokens: amount out for a given amount in (30BP fee). + * get_amount_out(amount_in, reserve_in, reserve_out). + */ +export function getAmountOut( + amountIn: bigint, + reserveIn: bigint, + reserveOut: bigint, +): bigint { + const feeAdjusted = amountIn * 997n; + const numerator = reserveOut * feeAdjusted; + const denominator = reserveIn * 1000n + feeAdjusted; + return numerator / denominator; +} + +/** + * Expected tokens for exact tokens (reverse quote): amount in needed to get amount_out. + * get_amount_in(amount_out, reserve_in, reserve_out). + */ +export function getAmountIn( + amountOut: bigint, + reserveIn: bigint, + reserveOut: bigint, +): bigint { + const amountOutScaled = amountOut * 1000n; + const numerator = reserveIn * amountOutScaled; + const denominator = (reserveOut - amountOut) * 997n; + if (denominator <= 0n) return 0n; + return numerator / denominator; +} diff --git a/src/pages/Launchpad.tsx b/src/pages/Launchpad.tsx index 269bb3b..a22a372 100644 --- a/src/pages/Launchpad.tsx +++ b/src/pages/Launchpad.tsx @@ -7,11 +7,19 @@ import { UnifiedWalletButton } from '@/components/UnifiedWalletButton'; import useLaunchpad, { getMidenscanAccountUrl, getMidenscanTxUrl, + LAUNCH_STEPS, + type LaunchStepIndex, type LaunchSuccess, } from '@/hooks/useLaunchpad'; import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; import { truncateId } from '@/lib/format'; -import { ArrowLeft, CheckCircle, ExternalLink, Loader2 } from 'lucide-react'; +import { + ArrowLeft, + CheckCircle, + CheckCircle2, + ExternalLink, + Loader2, +} from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { parseUnits } from 'viem'; @@ -61,7 +69,7 @@ export default function Launchpad() { const { connected } = useUnifiedWallet(); const { launchToken, error, clearError } = useLaunchpad(); const [symbol, setSymbol] = useState(''); - const [decimals, setDecimals] = useState('6'); + const [decimals, setDecimals] = useState('4'); const [initialSupply, setInitialSupply] = useState(''); const [touched, setTouched] = useState({ symbol: false, @@ -69,6 +77,7 @@ export default function Launchpad() { supply: false, }); const [isSubmitting, setIsSubmitting] = useState(false); + const [launchStep, setLaunchStep] = useState(null); const [successResult, setSuccessResult] = useState(null); const [copiedId, setCopiedId] = useState<'tx' | 'faucet' | null>(null); @@ -78,7 +87,7 @@ export default function Launchpad() { ? validateDecimals(decimalsNum) : (Number.isNaN(decimalsNum) ? 'Invalid number' : null); const supplyError = touched.supply - ? validateInitialSupply(initialSupply, Number.isNaN(decimalsNum) ? 18 : decimalsNum) + ? validateInitialSupply(initialSupply, Number.isNaN(decimalsNum) ? 4 : decimalsNum) : null; const canSubmit = connected @@ -107,17 +116,24 @@ export default function Launchpad() { } if (supplyBigint <= 0n) return; setIsSubmitting(true); + setLaunchStep(null); clearError(); - const result = await launchToken({ - symbol: sym, - decimals: dec, - initialSupply: supplyBigint, - }); + const result = await launchToken( + { + symbol: sym, + decimals: dec, + initialSupply: supplyBigint, + }, + { + onProgress: (step) => setLaunchStep(step), + }, + ); setIsSubmitting(false); + setLaunchStep(null); if (result) { setSuccessResult(result); setSymbol(''); - setDecimals('6'); + setDecimals('4'); setInitialSupply(''); setTouched({ symbol: false, decimals: false, supply: false }); clearError(); @@ -156,6 +172,10 @@ export default function Launchpad() {

Create a new faucet token and mint initial supply to your wallet.

+

+ Launching a new token can take a couple of seconds. Please wait until the + process completes. +

{successResult @@ -337,6 +357,62 @@ export default function Launchpad() { )}
+ {isSubmitting && launchStep !== null && ( +
+

Progress

+
+
+
+
    + {LAUNCH_STEPS.map((label, i) => { + const done = i < launchStep; + const current = i === launchStep; + return ( +
  • + {done + ? ( + + ) + : current + ? ( + + ) + : ( + + )} + + {label} + +
  • + ); + })} +
+
+ )} + {error && (
BigInt(0); const openXykModal = useCallback( @@ -75,13 +76,14 @@ export default function XykPoolDetail() { const pairLabel = `${poolData.token0.symbol} / ${poolData.token1.symbol}`; const feeTier = feeTierForSymbol(poolData.token0.symbol); - const priceDisplay = - poolData.priceToken0InToken1 > 0 - ? `1 ${poolData.token0.symbol} = ${poolData.priceToken0InToken1.toFixed(6)} ${poolData.token1.symbol}` - : '—'; + const priceDisplay = poolData.priceToken0InToken1 > 0 + ? `1 ${poolData.token0.symbol} = ${ + poolData.priceToken0InToken1.toFixed(6) + } ${poolData.token1.symbol}` + : '—'; const totalSupplyFormatted = fullNumberBigintFormat({ value: poolData.totalSupply, - expo: 18, + expo: 0, }); const assetSymbol = (faucetIdBech32: string) => { @@ -90,8 +92,12 @@ export default function XykPoolDetail() { return null; }; const assetDecimals = (faucetIdBech32: string) => { - if (faucetIdBech32 === poolData.token0.faucetIdBech32) return poolData.token0.decimals; - if (faucetIdBech32 === poolData.token1.faucetIdBech32) return poolData.token1.decimals; + if (faucetIdBech32 === poolData.token0.faucetIdBech32) { + return poolData.token0.decimals; + } + if (faucetIdBech32 === poolData.token1.faucetIdBech32) { + return poolData.token1.decimals; + } return 18; }; @@ -176,53 +182,76 @@ export default function XykPoolDetail() { Pool notes - {notesLoading ? ( -

Loading notes…

- ) : notesError ? ( -

{notesError.message}

- ) : poolNotes.length === 0 ? ( -

No notes issued by this pool yet.

- ) : ( -
- - - - - - - - - - {poolNotes.map((row) => ( - - - - + {notesLoading + ? ( +

+ Loading notes… +

+ ) + : notesError + ? ( +

+ {notesError.message} +

+ ) + : poolNotes.length === 0 + ? ( +

+ No notes issued by this pool yet. +

+ ) + : ( +
+
Note IDAssetsImplied price
- {row.noteId.slice(0, 10)}…{row.noteId.slice(-6)} - - - {row.assets.map((a) => { - const sym = assetSymbol(a.faucetIdBech32); - return ( - - - {prettyBigintFormat({ value: a.amount, expo: assetDecimals(a.faucetIdBech32) })} - {sym ? ` ${sym}` : ''} - - ); - })} - - - {row.impliedPrice != null - ? `1 ${poolData.token0.symbol} = ${row.impliedPrice.toFixed(6)} ${poolData.token1.symbol}` - : '—'} -
+ + + + + - ))} - -
Note IDAssetsImplied price
-
- )} + + + {poolNotes.map((row) => ( + + + {row.noteId.slice(0, 10)}…{row.noteId.slice(-6)} + + + + {row.assets.map((a) => { + const sym = assetSymbol(a.faucetIdBech32); + return ( + + + {prettyBigintFormat({ + value: a.amount, + expo: assetDecimals(a.faucetIdBech32), + })} + {sym ? ` ${sym}` : ''} + + ); + })} + + + + {row.impliedPrice != null + ? `1 ${poolData.token0.symbol} = ${ + row.impliedPrice.toFixed(6) + } ${poolData.token1.symbol}` + : '—'} + + + ))} + + +
+ )} From c5805408a5f38f047ae180b1798e70ce4e4d074f Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 12 Mar 2026 11:16:27 +0100 Subject: [PATCH 19/49] Local withdraw works --- src/components/xyk-wizard/XykWizard.tsx | 23 ++++++++++++++++------- src/hooks/useXykWithdraw.ts | 20 +++++++++++++++++++- src/lib/DeployXykPool.ts | 10 +++++----- src/lib/XykDepositNote.ts | 10 +++++----- src/lib/XykWithdrawNote.ts | 13 +++++++------ 5 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index ab8af2d..260aaa6 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -15,7 +15,12 @@ import { compileXykDepositTransaction } from '@/lib/XykDepositNote'; import { type TokenConfigWithBalance, ZoroContext } from '@/providers/ZoroContext'; import { type TokenConfig } from '@/providers/ZoroProvider'; import { TransactionType } from '@demox-labs/miden-wallet-adapter'; -import { AccountId } from '@miden-sdk/miden-sdk'; +import { + AccountId, + NoteAndArgs, + NoteAndArgsArray, + TransactionRequestBuilder, +} from '@miden-sdk/miden-sdk'; import { AlertCircle, ChevronLeft, Loader2 } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -216,7 +221,7 @@ const XykWizard = () => { const { newPoolId } = await deployNewPool({ client, token0, token1 }); - const { tx } = await compileXykDepositTransaction({ + const { tx, note } = await compileXykDepositTransaction({ token0, token1, amount0, @@ -230,11 +235,15 @@ const XykWizard = () => { payload: tx, }); await client.syncState(); - // const consumeTx = new TransactionRequestBuilder().withInputNotes( - // new NoteAndArgsArray([new NoteAndArgs(note, null)]), - // ).build(); - // await client.submitNewTransaction(newPoolId, consumeTx); - // await client.syncState(); + + // For testing with public acc + const consumeTx = new TransactionRequestBuilder().withInputNotes( + new NoteAndArgsArray([new NoteAndArgs(note, null)]), + ).build(); + await client.submitNewTransaction(newPoolId, consumeTx); + // + + await client.syncState(); console.log('Deposited, tx of deposit: ', txId); return newPoolId; } catch (e) { diff --git a/src/hooks/useXykWithdraw.ts b/src/hooks/useXykWithdraw.ts index 1bad9f7..d19dc31 100644 --- a/src/hooks/useXykWithdraw.ts +++ b/src/hooks/useXykWithdraw.ts @@ -5,6 +5,12 @@ import { bech32ToAccountId } from '@/lib/utils'; import { compileXykWithdrawTransaction } from '@/lib/XykWithdrawNote'; import { ZoroContext } from '@/providers/ZoroContext'; import { TransactionType } from '@demox-labs/miden-wallet-adapter'; +import { + NoteAndArgs, + NoteAndArgsArray, + NoteRecipientArray, + TransactionRequestBuilder, +} from '@miden-sdk/miden-sdk'; import { useCallback, useContext, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; @@ -36,7 +42,7 @@ export function useXykWithdraw(poolId: string | undefined) { setIsLoading(true); try { await syncState(); - const { tx, noteId: nid } = await clientMutex.runExclusive(() => + const { tx, noteId: nid, note: withdrawNote, returnNote } = await clientMutex.runExclusive(() => compileXykWithdrawTransaction({ poolAccountId, userAccountId: accountId, @@ -53,6 +59,18 @@ export function useXykWithdraw(poolId: string | undefined) { await syncState(); setNoteId(nid); setTxId(txIdResult); + + const consumeReq = new TransactionRequestBuilder() + .withInputNotes( + new NoteAndArgsArray([new NoteAndArgs(withdrawNote, null)]), + ) + .withExpectedOutputRecipients( + new NoteRecipientArray([returnNote.recipient()]), + ) + .build(); + await client.submitNewTransaction(poolAccountId, consumeReq); + await syncState(); + return { noteId: nid, txId: txIdResult }; } catch (err) { console.error(err); diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 295da91..c15ef04 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -113,9 +113,9 @@ export async function deployNewPool({ console.log('lp local build'); const lp_local_lib = build_lp_local_lib(client); // console.log('c prod build'); - // const c_prod_lib = build_c_prod_lib(client); + // const xyk_pool_lib = build_xyk_pool_lib(client); - // const c_prod_pool_component = AccountComponent.fromLibrary(c_prod_lib, [ + // const xyk_pool_component = AccountComponent.fromLibrary(xyk_pool_lib, [ // reserve_slot, // assets_mapping_slot, // ]); @@ -139,12 +139,12 @@ export async function deployNewPool({ const contract = new AccountBuilder(walletSeed) .accountType(AccountType.RegularAccountImmutableCode) - .storageMode(AccountStorageMode.network()) - // .storageMode(AccountStorageMode.public()) + // .storageMode(AccountStorageMode.network()) + .storageMode(AccountStorageMode.public()) .withNoAuthComponent() .withComponent(lp_local_component) // .withAuthComponent(authComponent) - // .withComponent(c_prod_pool_component) + // .withComponent(xyk_pool_component) .withBasicWalletComponent() .build(); diff --git a/src/lib/XykDepositNote.ts b/src/lib/XykDepositNote.ts index 1dd0a12..e181acf 100644 --- a/src/lib/XykDepositNote.ts +++ b/src/lib/XykDepositNote.ts @@ -55,16 +55,16 @@ export async function compileXykDepositTransaction({ const noteTag = NoteTag.withAccountTarget(poolAccountId); - const attachment = NoteAttachment.newNetworkAccountTarget( - poolAccountId, - NoteExecutionHint.always(), - ); + // const attachment = NoteAttachment.newNetworkAccountTarget( + // poolAccountId, + // NoteExecutionHint.always(), + // ); const metadata = new NoteMetadata( userAccountId, NoteType.Public, noteTag, - ).withAttachment(attachment); + ); // .withAttachment(attachment); const inputs = new NoteInputs( new FeltArray([ diff --git a/src/lib/XykWithdrawNote.ts b/src/lib/XykWithdrawNote.ts index 9671f92..855c51c 100644 --- a/src/lib/XykWithdrawNote.ts +++ b/src/lib/XykWithdrawNote.ts @@ -57,15 +57,15 @@ export async function compileXykWithdrawTransaction({ ); const noteTag = NoteTag.withAccountTarget(poolAccountId); - const attachment = NoteAttachment.newNetworkAccountTarget( - poolAccountId, - NoteExecutionHint.always(), - ); + // const attachment = NoteAttachment.newNetworkAccountTarget( + // poolAccountId, + // NoteExecutionHint.always(), + // ); const metadata = new NoteMetadata( userAccountId, NoteType.Public, noteTag, - ).withAttachment(attachment); + ); // .withAttachment(attachment); const returnNoteTag = NoteTag.withAccountTarget(userAccountId); const returnNoteType = NoteType.Public; @@ -89,7 +89,7 @@ export async function compileXykWithdrawTransaction({ new Felt(BigInt(0)), new Felt(lpAmount), new Felt(BigInt(returnNoteTag.asU32())), - new Felt(BigInt(returnNoteType)), + new Felt(BigInt(NoteType.Public)), new Felt(BigInt(0)), new Felt(BigInt(0)), returnNoteRecipientDigest[0], @@ -126,5 +126,6 @@ export async function compileXykWithdrawTransaction({ tx, noteId, note, + returnNote, }; } From b9fbeeac006d78c13c1b96a8adbd2f118bb1ffb5 Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 12 Mar 2026 11:42:20 +0100 Subject: [PATCH 20/49] network acc --- src/components/xyk-wizard/XykWizard.tsx | 17 ++++++--------- src/hooks/useXykWithdraw.ts | 28 ++++++++++--------------- src/lib/DeployXykPool.ts | 6 +++--- src/lib/XykDepositNote.ts | 10 ++++----- src/lib/XykWithdrawNote.ts | 14 +++++-------- 5 files changed, 30 insertions(+), 45 deletions(-) diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index 260aaa6..e4fbc28 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -15,12 +15,7 @@ import { compileXykDepositTransaction } from '@/lib/XykDepositNote'; import { type TokenConfigWithBalance, ZoroContext } from '@/providers/ZoroContext'; import { type TokenConfig } from '@/providers/ZoroProvider'; import { TransactionType } from '@demox-labs/miden-wallet-adapter'; -import { - AccountId, - NoteAndArgs, - NoteAndArgsArray, - TransactionRequestBuilder, -} from '@miden-sdk/miden-sdk'; +import { AccountId } from '@miden-sdk/miden-sdk'; import { AlertCircle, ChevronLeft, Loader2 } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -221,7 +216,7 @@ const XykWizard = () => { const { newPoolId } = await deployNewPool({ client, token0, token1 }); - const { tx, note } = await compileXykDepositTransaction({ + const { tx } = await compileXykDepositTransaction({ token0, token1, amount0, @@ -237,10 +232,10 @@ const XykWizard = () => { await client.syncState(); // For testing with public acc - const consumeTx = new TransactionRequestBuilder().withInputNotes( - new NoteAndArgsArray([new NoteAndArgs(note, null)]), - ).build(); - await client.submitNewTransaction(newPoolId, consumeTx); + // const consumeTx = new TransactionRequestBuilder().withInputNotes( + // new NoteAndArgsArray([new NoteAndArgs(note, null)]), + // ).build(); + // await client.submitNewTransaction(newPoolId, consumeTx); // await client.syncState(); diff --git a/src/hooks/useXykWithdraw.ts b/src/hooks/useXykWithdraw.ts index d19dc31..3d21971 100644 --- a/src/hooks/useXykWithdraw.ts +++ b/src/hooks/useXykWithdraw.ts @@ -5,12 +5,6 @@ import { bech32ToAccountId } from '@/lib/utils'; import { compileXykWithdrawTransaction } from '@/lib/XykWithdrawNote'; import { ZoroContext } from '@/providers/ZoroContext'; import { TransactionType } from '@demox-labs/miden-wallet-adapter'; -import { - NoteAndArgs, - NoteAndArgsArray, - NoteRecipientArray, - TransactionRequestBuilder, -} from '@miden-sdk/miden-sdk'; import { useCallback, useContext, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; @@ -42,7 +36,7 @@ export function useXykWithdraw(poolId: string | undefined) { setIsLoading(true); try { await syncState(); - const { tx, noteId: nid, note: withdrawNote, returnNote } = await clientMutex.runExclusive(() => + const { tx, noteId: nid } = await clientMutex.runExclusive(() => compileXykWithdrawTransaction({ poolAccountId, userAccountId: accountId, @@ -60,16 +54,16 @@ export function useXykWithdraw(poolId: string | undefined) { setNoteId(nid); setTxId(txIdResult); - const consumeReq = new TransactionRequestBuilder() - .withInputNotes( - new NoteAndArgsArray([new NoteAndArgs(withdrawNote, null)]), - ) - .withExpectedOutputRecipients( - new NoteRecipientArray([returnNote.recipient()]), - ) - .build(); - await client.submitNewTransaction(poolAccountId, consumeReq); - await syncState(); + // const consumeReq = new TransactionRequestBuilder() + // .withInputNotes( + // new NoteAndArgsArray([new NoteAndArgs(withdrawNote, null)]), + // ) + // .withExpectedOutputRecipients( + // new NoteRecipientArray([returnNote.recipient()]), + // ) + // .build(); + // await client.submitNewTransaction(poolAccountId, consumeReq); + // await syncState(); return { noteId: nid, txId: txIdResult }; } catch (err) { diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index c15ef04..955ce9b 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -130,7 +130,7 @@ export async function deployNewPool({ ], ).withSupportsAllTypes(); - console.log('account build'); + console.log('account built'); const walletSeed = new Uint8Array(32); crypto.getRandomValues(walletSeed); @@ -139,8 +139,8 @@ export async function deployNewPool({ const contract = new AccountBuilder(walletSeed) .accountType(AccountType.RegularAccountImmutableCode) - // .storageMode(AccountStorageMode.network()) - .storageMode(AccountStorageMode.public()) + .storageMode(AccountStorageMode.network()) + // .storageMode(AccountStorageMode.public()) .withNoAuthComponent() .withComponent(lp_local_component) // .withAuthComponent(authComponent) diff --git a/src/lib/XykDepositNote.ts b/src/lib/XykDepositNote.ts index e181acf..1dd0a12 100644 --- a/src/lib/XykDepositNote.ts +++ b/src/lib/XykDepositNote.ts @@ -55,16 +55,16 @@ export async function compileXykDepositTransaction({ const noteTag = NoteTag.withAccountTarget(poolAccountId); - // const attachment = NoteAttachment.newNetworkAccountTarget( - // poolAccountId, - // NoteExecutionHint.always(), - // ); + const attachment = NoteAttachment.newNetworkAccountTarget( + poolAccountId, + NoteExecutionHint.always(), + ); const metadata = new NoteMetadata( userAccountId, NoteType.Public, noteTag, - ); // .withAttachment(attachment); + ).withAttachment(attachment); const inputs = new NoteInputs( new FeltArray([ diff --git a/src/lib/XykWithdrawNote.ts b/src/lib/XykWithdrawNote.ts index 855c51c..55d49d2 100644 --- a/src/lib/XykWithdrawNote.ts +++ b/src/lib/XykWithdrawNote.ts @@ -8,14 +8,10 @@ import { Note, NoteAssets, NoteAttachment, - NoteDetails, - NoteDetailsAndTag, - NoteDetailsAndTagArray, NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, - NoteRecipientArray, NoteTag, NoteType, OutputNote, @@ -57,15 +53,15 @@ export async function compileXykWithdrawTransaction({ ); const noteTag = NoteTag.withAccountTarget(poolAccountId); - // const attachment = NoteAttachment.newNetworkAccountTarget( - // poolAccountId, - // NoteExecutionHint.always(), - // ); + const attachment = NoteAttachment.newNetworkAccountTarget( + poolAccountId, + NoteExecutionHint.always(), + ); const metadata = new NoteMetadata( userAccountId, NoteType.Public, noteTag, - ); // .withAttachment(attachment); + ).withAttachment(attachment); const returnNoteTag = NoteTag.withAccountTarget(userAccountId); const returnNoteType = NoteType.Public; From 06a1388a316e8a35e3af58377406d65f5cdddfac Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 12 Mar 2026 12:28:58 +0100 Subject: [PATCH 21/49] wip swapping xyk --- src/hooks/useXykWithdraw.ts | 2 ++ src/lib/XykSwapNote.ts | 14 -------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/hooks/useXykWithdraw.ts b/src/hooks/useXykWithdraw.ts index 3d21971..0a5c141 100644 --- a/src/hooks/useXykWithdraw.ts +++ b/src/hooks/useXykWithdraw.ts @@ -54,6 +54,8 @@ export function useXykWithdraw(poolId: string | undefined) { setNoteId(nid); setTxId(txIdResult); + console.log(txIdResult, nid); + // const consumeReq = new TransactionRequestBuilder() // .withInputNotes( // new NoteAndArgsArray([new NoteAndArgs(withdrawNote, null)]), diff --git a/src/lib/XykSwapNote.ts b/src/lib/XykSwapNote.ts index 0917727..627f0ae 100644 --- a/src/lib/XykSwapNote.ts +++ b/src/lib/XykSwapNote.ts @@ -8,14 +8,10 @@ import { Note, NoteAssets, NoteAttachment, - NoteDetails, - NoteDetailsAndTag, - NoteDetailsAndTagArray, NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, - NoteRecipientArray, NoteTag, NoteType, OutputNote, @@ -77,7 +73,6 @@ export async function compileXykSwapExactTokensForTokensTransaction({ returnNoteType, new NoteAttachment(), ); - const returnNoteRecipient = returnNote.recipient(); const returnNoteRecipientDigest = returnNote.recipient().digest().toFelts(); const inputs = new NoteInputs( @@ -108,15 +103,6 @@ export async function compileXykSwapExactTokensForTokensTransaction({ const transactionRequest = new TransactionRequestBuilder() .withOwnOutputNotes(new MidenArrays.OutputNoteArray([OutputNote.full(note)])) - .withExpectedOutputRecipients(new NoteRecipientArray([returnNoteRecipient])) - .withExpectedFutureNotes( - new NoteDetailsAndTagArray([ - new NoteDetailsAndTag( - new NoteDetails(noteAssets, returnNoteRecipient), - returnNoteTag, - ), - ]), - ) .build(); const tx = new CustomTransaction( From 31831f55df054303b21c0ae1f9f9b5a8cc557c93 Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 12 Mar 2026 13:05:09 +0100 Subject: [PATCH 22/49] Swapping on xyk detail --- src/hooks/useXykSwap.ts | 84 ++++++++++++++ src/lib/DeployXykPool.ts | 14 +-- src/lib/XykSwapNote.ts | 54 +++++---- src/lib/ZoroSwapNote.ts | 2 + src/pages/XykPoolDetail.tsx | 226 +++++++++++++++++++++++++++++++++++- 5 files changed, 343 insertions(+), 37 deletions(-) create mode 100644 src/hooks/useXykSwap.ts diff --git a/src/hooks/useXykSwap.ts b/src/hooks/useXykSwap.ts new file mode 100644 index 0000000..bcc1744 --- /dev/null +++ b/src/hooks/useXykSwap.ts @@ -0,0 +1,84 @@ +import { clientMutex } from '@/lib/clientMutex'; +import { compileXykSwapTransaction } from '@/lib/XykSwapNote'; +import { bech32ToAccountId } from '@/lib/utils'; +import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; +import { useXykPool } from '@/hooks/useXykPool'; +import { ZoroContext } from '@/providers/ZoroContext'; +import { TransactionType } from '@demox-labs/miden-wallet-adapter'; +import type { AccountId } from '@miden-sdk/miden-sdk'; +import { useCallback, useContext, useMemo, useState } from 'react'; +import { toast } from 'react-toastify'; + +export function useXykSwap(poolId: string | undefined) { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + const [txId, setTxId] = useState(); + const [noteId, setNoteId] = useState(); + const { requestTransaction } = useUnifiedWallet(); + const { client, accountId, syncState } = useContext(ZoroContext); + const { data: poolData } = useXykPool(poolId); + + const swap = useCallback( + async ( + sellToken: AccountId, + buyToken: AccountId, + amount: bigint, + minAmountOut: bigint, + ): Promise<{ noteId: string; txId: string | undefined } | undefined> => { + if ( + !poolId || + !poolData || + !client || + !accountId || + !requestTransaction + ) { + return undefined; + } + const poolAccountId = bech32ToAccountId(poolId); + if (!poolAccountId) return undefined; + setError(''); + setIsLoading(true); + try { + await syncState(); + const { tx, noteId: nid } = await clientMutex.runExclusive(() => + compileXykSwapTransaction({ + poolAccountId, + userAccountId: accountId, + sellToken, + buyToken, + amount, + minAmountOut, + client, + }), + ); + const txIdResult = await requestTransaction({ + type: TransactionType.Custom, + payload: tx, + }); + await syncState(); + setNoteId(nid); + setTxId(txIdResult); + return { noteId: nid, txId: txIdResult }; + } catch (err) { + console.error(err); + toast.error(`Error swapping: ${err}`); + } finally { + setIsLoading(false); + } + return undefined; + }, + [ + poolId, + poolData, + client, + accountId, + requestTransaction, + syncState, + ], + ); + + return useMemo( + () => ({ swap, isLoading, error, txId, noteId }), + [swap, isLoading, error, txId, noteId], + ); +} diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 955ce9b..8127047 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -52,17 +52,13 @@ export const build_lp_local_lib = (client: WebClient) => { return builder.buildLibrary('zoro::lp_local', lp_local); }; export const build_xyk_pool_lib = (client: WebClient) => { - console.log('math'); const math_lib = build_math_lib(client); - console.log('storage_utils'); // const storage_utils = build_storage_utils(client); - console.log('lp_local'); const lp_local = build_lp_local_lib(client); const builder = client.createCodeBuilder(); builder.linkStaticLibrary(math_lib); // builder.linkStaticLibrary(storage_utils); builder.linkStaticLibrary(lp_local); - console.log('c_prod'); return builder.buildLibrary('zoro::xyk_pool', xyk_pool); }; @@ -113,12 +109,10 @@ export async function deployNewPool({ console.log('lp local build'); const lp_local_lib = build_lp_local_lib(client); // console.log('c prod build'); - // const xyk_pool_lib = build_xyk_pool_lib(client); + const xyk_pool_lib = build_xyk_pool_lib(client); - // const xyk_pool_component = AccountComponent.fromLibrary(xyk_pool_lib, [ - // reserve_slot, - // assets_mapping_slot, - // ]); + const xyk_pool_component = AccountComponent.fromLibrary(xyk_pool_lib, []) + .withSupportsAllTypes(); const lp_local_component = AccountComponent.fromLibrary( lp_local_lib, @@ -144,7 +138,7 @@ export async function deployNewPool({ .withNoAuthComponent() .withComponent(lp_local_component) // .withAuthComponent(authComponent) - // .withComponent(xyk_pool_component) + .withComponent(xyk_pool_component) .withBasicWalletComponent() .build(); diff --git a/src/lib/XykSwapNote.ts b/src/lib/XykSwapNote.ts index 627f0ae..064c534 100644 --- a/src/lib/XykSwapNote.ts +++ b/src/lib/XykSwapNote.ts @@ -20,69 +20,70 @@ import { } from '@miden-sdk/miden-sdk'; import SCRIPT from '@/masm/notes/xyk_swap_exact_tokens_for_tokens.masm?raw'; +import { build_xyk_pool_lib } from './DeployXykPool'; import { accountIdToBech32, generateRandomSerialNumber } from './utils'; -export interface SwapParams { - token0: AccountId; - token1: AccountId; - amount: bigint; - userAccountId: AccountId; +export interface XykSwapParams { poolAccountId: AccountId; + userAccountId: AccountId; + sellToken: AccountId; + buyToken: AccountId; + amount: bigint; + minAmountOut: bigint; client: WebClient; } -export interface SwapResult { - readonly txId: string; - readonly noteId: string; -} - -export async function compileXykSwapExactTokensForTokensTransaction({ +export async function compileXykSwapTransaction({ poolAccountId, userAccountId, - token0, - token1, + sellToken, + buyToken, amount, + minAmountOut, client, -}: SwapParams) { +}: XykSwapParams) { + const xyk_pool_lib = build_xyk_pool_lib(client); const builder = client.createCodeBuilder(); - const script = builder.compileNoteScript( - SCRIPT, - ); + builder.linkStaticLibrary(xyk_pool_lib); + const script = builder.compileNoteScript(SCRIPT); const noteTag = NoteTag.withAccountTarget(poolAccountId); const attachment = NoteAttachment.newNetworkAccountTarget( poolAccountId, NoteExecutionHint.always(), ); - const metadata = new NoteMetadata( userAccountId, NoteType.Public, noteTag, ).withAttachment(attachment); + // Return note: P2ID to user's public account (no network attachment). const returnNoteTag = NoteTag.withAccountTarget(userAccountId); const returnNoteType = NoteType.Public; - const noteAssets = new NoteAssets([ - new FungibleAsset(token0, BigInt(amount)), + const returnNoteAssets = new NoteAssets([ + new FungibleAsset(buyToken, BigInt(1)), ]); const returnNote = Note.createP2IDNote( poolAccountId, userAccountId, - noteAssets, + returnNoteAssets, returnNoteType, new NoteAttachment(), ); const returnNoteRecipientDigest = returnNote.recipient().digest().toFelts(); + const deadline = Date.now() + 120_000; // 2 min + const inputs = new NoteInputs( new FeltArray([ - token1.prefix(), - token1.suffix(), new Felt(BigInt(0)), - new Felt(BigInt(0)), // deadline ????? + new Felt(BigInt(0)), + new Felt(BigInt(0)), + new Felt(minAmountOut), + new Felt(BigInt(deadline)), new Felt(BigInt(returnNoteTag.asU32())), - new Felt(BigInt(returnNoteType)), + new Felt(BigInt(NoteType.Public)), new Felt(BigInt(0)), returnNoteRecipientDigest[0], returnNoteRecipientDigest[1], @@ -91,6 +92,9 @@ export async function compileXykSwapExactTokensForTokensTransaction({ ]), ); + const noteAssets = new NoteAssets([ + new FungibleAsset(sellToken, amount), + ]); const note = new Note( noteAssets, metadata, diff --git a/src/lib/ZoroSwapNote.ts b/src/lib/ZoroSwapNote.ts index ba64f18..bf8e6b6 100644 --- a/src/lib/ZoroSwapNote.ts +++ b/src/lib/ZoroSwapNote.ts @@ -95,6 +95,8 @@ export async function compileSwapTransaction({ const noteId = note.id().toString(); + console.log('Swap note: ', noteId); + const transactionRequest = new TransactionRequestBuilder() .withOwnOutputNotes(new MidenArrays.OutputNoteArray([OutputNote.full(note)])) .build(); diff --git a/src/pages/XykPoolDetail.tsx b/src/pages/XykPoolDetail.tsx index c4f55b0..48e40d1 100644 --- a/src/pages/XykPoolDetail.tsx +++ b/src/pages/XykPoolDetail.tsx @@ -6,16 +6,25 @@ import { PoolDetailLayout } from '@/components/PoolDetailLayout'; import { PoolDetailStats } from '@/components/PoolDetailStats'; import { PoolInfoCard } from '@/components/PoolInfoCard'; import { RecentTransactionsCard } from '@/components/RecentTransactionsCard'; +import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; import { XykPoolModal } from '@/components/XykPoolModal'; import { useXykLpBalance } from '@/hooks/useXykLpBalance'; import { useXykPool } from '@/hooks/useXykPool'; import { useXykPoolNotes } from '@/hooks/useXykPoolNotes'; -import { fullNumberBigintFormat, prettyBigintFormat } from '@/lib/format'; +import { useXykSwap } from '@/hooks/useXykSwap'; +import { useBalance } from '@/hooks/useBalance'; +import { fullNumberBigintFormat, formatTokenAmountForInput, prettyBigintFormat } from '@/lib/format'; import { getMockRecentTransactions } from '@/mocks/poolDetailMocks'; import { ModalContext } from '@/providers/ModalContext'; -import { useCallback, useContext, useMemo } from 'react'; +import type { XykTokenInfo } from '@/hooks/useXykPool'; +import { getAmountOut } from '@/lib/xykMath'; +import type { TokenConfig } from '@/providers/ZoroProvider'; +import { useCallback, useContext, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; +import { ArrowDownUp, Loader2 } from 'lucide-react'; +import { parseUnits } from 'viem'; const feeTierForSymbol = (_symbol: string) => '0.30%'; @@ -32,8 +41,111 @@ export default function XykPoolDetail() { decodedPoolId, poolData ?? null, ); + const { swap, isLoading: isSwapLoading } = useXykSwap(decodedPoolId); + const [swapSellSide, setSwapSellSide] = useState<0 | 1>(0); + const [amountInStr, setAmountInStr] = useState(''); + const [swapInputError, setSwapInputError] = useState(); const hasPosition = lpBalance > BigInt(0); + const xykTokenToConfig = useCallback((t: XykTokenInfo): TokenConfig => ({ + symbol: t.symbol, + name: t.name ?? t.symbol, + decimals: t.decimals, + faucetId: t.faucetId, + faucetIdBech32: t.faucetIdBech32, + oracleId: '', + }), []); + + const swapToken0Config = useMemo( + () => (poolData ? xykTokenToConfig(poolData.token0) : undefined), + [poolData, xykTokenToConfig], + ); + const swapToken1Config = useMemo( + () => (poolData ? xykTokenToConfig(poolData.token1) : undefined), + [poolData, xykTokenToConfig], + ); + const { balance: balanceToken0 } = useBalance({ token: swapToken0Config }); + const { balance: balanceToken1 } = useBalance({ token: swapToken1Config }); + + const swapSellToken = useMemo( + () => (poolData ? (swapSellSide === 0 ? poolData.token0 : poolData.token1) : null), + [poolData, swapSellSide], + ); + const swapBuyToken = useMemo( + () => (poolData ? (swapSellSide === 0 ? poolData.token1 : poolData.token0) : null), + [poolData, swapSellSide], + ); + + const sellBalance = swapSellSide === 0 ? balanceToken0 : balanceToken1; + const buyBalance = swapSellSide === 0 ? balanceToken1 : balanceToken0; + + const amountInBigint = useMemo(() => { + if (!swapSellToken || !amountInStr.trim()) return 0n; + try { + return parseUnits(amountInStr.trim(), swapSellToken.decimals); + } catch { + return 0n; + } + }, [amountInStr, swapSellToken]); + + const expectedAmountOut = useMemo(() => { + if (!poolData || !swapSellToken || !swapBuyToken || amountInBigint <= 0n) return 0n; + const [reserveIn, reserveOut] = + swapSellSide === 0 + ? [poolData.reserve0, poolData.reserve1] + : [poolData.reserve1, poolData.reserve0]; + return getAmountOut(amountInBigint, reserveIn, reserveOut); + }, [poolData, swapSellSide, swapSellToken, swapBuyToken, amountInBigint]); + + const expectedAmountOutStr = useMemo( + () => + swapBuyToken && expectedAmountOut >= 0n + ? formatTokenAmountForInput({ + value: expectedAmountOut, + expo: swapBuyToken.decimals, + }) + : '', + [swapBuyToken, expectedAmountOut], + ); + + const onSwapDirection = useCallback(() => { + setSwapSellSide((s) => (s === 0 ? 1 : 0)); + setAmountInStr(''); + setSwapInputError(undefined); + }, []); + + const SWAP_PERCENTAGES = [25, 50, 75, 100] as const; + const setSellAmountPct = useCallback( + (pct: number) => { + if (!swapSellToken || sellBalance == null || sellBalance <= 0n) return; + const amount = (sellBalance * BigInt(pct)) / 100n; + setAmountInStr( + formatTokenAmountForInput({ + value: amount, + expo: swapSellToken.decimals, + }), + ); + setSwapInputError(undefined); + }, + [swapSellToken, sellBalance], + ); + + const onExecuteSwap = useCallback(async () => { + if (!poolData || !swapSellToken || !swapBuyToken) return; + setSwapInputError(undefined); + if (amountInBigint <= 0n) { + setSwapInputError('Enter amount to sell'); + return; + } + await swap( + swapSellToken.faucetId, + swapBuyToken.faucetId, + amountInBigint, + expectedAmountOut, + ); + setAmountInStr(''); + }, [poolData, swapSellToken, swapBuyToken, amountInBigint, expectedAmountOut, swap]); + const openXykModal = useCallback( (mode: 'Deposit' | 'Withdraw') => { if (!decodedPoolId) return; @@ -142,6 +254,116 @@ export default function XykPoolDetail() { decimals0={poolData.token0.decimals} decimals1={poolData.token1.decimals} /> + + + Swap + + +
+
+ From + {swapSellToken && sellBalance != null && ( + + Balance:{' '} + {prettyBigintFormat({ + value: sellBalance, + expo: swapSellToken.decimals, + })}{' '} + {swapSellToken.symbol} + + )} +
+
+ + { + setAmountInStr(e.target.value); + setSwapInputError(undefined); + }} + className='border-0 bg-transparent shadow-none focus-visible:ring-0' + /> + + {swapSellToken?.symbol ?? '—'} + +
+
+ {SWAP_PERCENTAGES.map((pct) => ( + + ))} +
+
+
+ +
+
+
+ To + {swapBuyToken && buyBalance != null && ( + + Balance:{' '} + {prettyBigintFormat({ + value: buyBalance, + expo: swapBuyToken.decimals, + })}{' '} + {swapBuyToken.symbol} + + )} +
+
+ + + {expectedAmountOutStr || '0'} + + + {swapBuyToken?.symbol ?? '—'} + +
+
+ {swapInputError && ( +

{swapInputError}

+ )} + +
+
Date: Thu, 12 Mar 2026 13:22:56 +0100 Subject: [PATCH 23/49] Hidden tables for now --- src/pages/XykPoolDetail.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/XykPoolDetail.tsx b/src/pages/XykPoolDetail.tsx index 48e40d1..148f151 100644 --- a/src/pages/XykPoolDetail.tsx +++ b/src/pages/XykPoolDetail.tsx @@ -399,6 +399,7 @@ export default function XykPoolDetail() {
+ {/* Pool notes @@ -477,6 +478,7 @@ export default function XykPoolDetail() { + */} { /* Date: Sat, 14 Mar 2026 16:13:06 +0100 Subject: [PATCH 24/49] Swapping XYK works, fix for reserves --- src/hooks/useXykPool.tsx | 10 +- src/lib/DeployXykPool.ts | 2 - src/lib/XykSwapNote.ts | 15 +- src/lib/XykWithdrawNote.ts | 10 +- src/masm/accounts/lp_local.masm | 191 ++++-------------- src/masm/accounts/xyk_pool.masm | 92 ++++++--- .../xyk_swap_exact_tokens_for_tokens.masm | 24 ++- .../xyk_swap_tokens_for_exact_tokens.masm | 22 +- src/masm/notes/xyk_withdraw.masm | 25 ++- src/pages/XykPoolDetail.tsx | 162 ++++++++------- src/pages/skeletons/XykPoolDetailSkeleton.tsx | 86 ++++++++ 11 files changed, 324 insertions(+), 315 deletions(-) create mode 100644 src/pages/skeletons/XykPoolDetailSkeleton.tsx diff --git a/src/hooks/useXykPool.tsx b/src/hooks/useXykPool.tsx index 3897a40..9d76eb3 100644 --- a/src/hooks/useXykPool.tsx +++ b/src/hooks/useXykPool.tsx @@ -30,7 +30,7 @@ export interface XykPoolData { } export function useXykPool(poolId: string | undefined) { - const { rpcClient, accountId } = useContext(ZoroContext); + const { rpcClient } = useContext(ZoroContext); const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -80,13 +80,13 @@ export function useXykPool(poolId: string | undefined) { } const token0Id = accountIdFromPrefixSuffix( - felts[3], - felts[2], - ); - const token1Id = accountIdFromPrefixSuffix( felts[1], felts[0], ); + const token1Id = accountIdFromPrefixSuffix( + felts[3], + felts[2], + ); const totalSupplyWord = storage.getItem('zoro::lp_local::total_supply'); const totalSupplyFelts = totalSupplyWord?.toFelts() ?? []; diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 8127047..663f33c 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -53,11 +53,9 @@ export const build_lp_local_lib = (client: WebClient) => { }; export const build_xyk_pool_lib = (client: WebClient) => { const math_lib = build_math_lib(client); - // const storage_utils = build_storage_utils(client); const lp_local = build_lp_local_lib(client); const builder = client.createCodeBuilder(); builder.linkStaticLibrary(math_lib); - // builder.linkStaticLibrary(storage_utils); builder.linkStaticLibrary(lp_local); return builder.buildLibrary('zoro::xyk_pool', xyk_pool); }; diff --git a/src/lib/XykSwapNote.ts b/src/lib/XykSwapNote.ts index 064c534..9e2f814 100644 --- a/src/lib/XykSwapNote.ts +++ b/src/lib/XykSwapNote.ts @@ -71,24 +71,23 @@ export async function compileXykSwapTransaction({ returnNoteType, new NoteAttachment(), ); - const returnNoteRecipientDigest = returnNote.recipient().digest().toFelts(); - + const p2id_root = returnNote.script().root().toFelts(); const deadline = Date.now() + 120_000; // 2 min const inputs = new NoteInputs( new FeltArray([ - new Felt(BigInt(0)), - new Felt(BigInt(0)), + new Felt(buyToken.prefix().asInt()), + new Felt(buyToken.suffix().asInt()), new Felt(BigInt(0)), new Felt(minAmountOut), new Felt(BigInt(deadline)), new Felt(BigInt(returnNoteTag.asU32())), new Felt(BigInt(NoteType.Public)), new Felt(BigInt(0)), - returnNoteRecipientDigest[0], - returnNoteRecipientDigest[1], - returnNoteRecipientDigest[2], - returnNoteRecipientDigest[3], + p2id_root[0], + p2id_root[1], + p2id_root[2], + p2id_root[3], ]), ); diff --git a/src/lib/XykWithdrawNote.ts b/src/lib/XykWithdrawNote.ts index 55d49d2..6ec2e22 100644 --- a/src/lib/XykWithdrawNote.ts +++ b/src/lib/XykWithdrawNote.ts @@ -76,7 +76,7 @@ export async function compileXykWithdrawTransaction({ returnNoteType, new NoteAttachment(), ); - const returnNoteRecipientDigest = returnNote.recipient().digest().toFelts(); + const root = returnNote.script().root().toFelts(); const inputs = new NoteInputs( new FeltArray([ @@ -88,10 +88,10 @@ export async function compileXykWithdrawTransaction({ new Felt(BigInt(NoteType.Public)), new Felt(BigInt(0)), new Felt(BigInt(0)), - returnNoteRecipientDigest[0], - returnNoteRecipientDigest[1], - returnNoteRecipientDigest[2], - returnNoteRecipientDigest[3], + root[0], + root[1], + root[2], + root[3], ]), ); diff --git a/src/masm/accounts/lp_local.masm b/src/masm/accounts/lp_local.masm index 0aa88a2..73a8f43 100644 --- a/src/masm/accounts/lp_local.masm +++ b/src/masm/accounts/lp_local.masm @@ -1,4 +1,6 @@ use miden::core::math::u64 +use miden::core::crypto::hashes::rpo256 +use miden::protocol::note use miden::protocol::native_account use miden::protocol::active_account use miden::protocol::output_note @@ -11,7 +13,8 @@ use zoro::storage_utils const MINIMUM_LIQUIDITY = 100 ### MEMORY LOCATIONS -const DYNAMIC_PROC_ADDR = 4 +const SCRATCH_SPACE = 0 +const DYNAMIC_PROC_ADDR = 16 const USER_ACCOUNT_ID_WORD = 20 const USER_ACCOUNT_ID_PREFIX = 20 @@ -21,7 +24,8 @@ const NOTE_DETAILS_WORD = 24 const NOTE_TAG = 24 const NOTE_TYPE = 25 -const NOTE_RECIPIENT_WORD = 28 +const OUTPUT_NOTE_ROOT_HASH = 28 +const INPUT_NOTE_SERIAL = 32 ### ERROR CODES const ERR_ZERO_ADDRESS = "Zero address" @@ -434,16 +438,18 @@ end #! Outputs: [amount_0_out, amount_1_out] #! @locals(6) -pub proc withdraw(lp_amount: word, user_id_prefix: felt, user_id_suffix: felt, note_tag: felt, note_type: felt, return_note_recipient: word) -> (felt, felt) +pub proc withdraw(lp_amount: word, user_id_prefix: felt, user_id_suffix: felt, note_tag: felt, note_type: felt, output_note_root: word, input_note_serial: word) -> (felt, felt) # loc.0: LP_AMOUNT = EMPTY, EMPTY, EMPTY, LP_AMOUNT loc.3 lp_amount # loc.4: amount_0_out # loc.5: amount_1_out ## LP_AMOUNT to local loc_storew_le.0 dropw - ## user, note tag and type tomemory - mem_store.USER_ACCOUNT_ID_PREFIX mem_store.USER_ACCOUNT_ID_SUFFIX mem_store.NOTE_TAG mem_store.NOTE_TYPE - ## RECIPIENT to memroy - mem_storew_le.NOTE_RECIPIENT_WORD dropw + ## user, note tag and type, output note root hash, and input note serial to memory + mem_store.USER_ACCOUNT_ID_PREFIX mem_store.USER_ACCOUNT_ID_SUFFIX + mem_store.NOTE_TAG mem_store.NOTE_TYPE + mem_storew_le.OUTPUT_NOTE_ROOT_HASH dropw + mem_storew_le.INPUT_NOTE_SERIAL dropw + # => [] ### get_lp_amount_out (total_supply: felt, amount_0: felt, amount_1: felt, reserve_0: felt, reserve_1: felt) exec.get_reserves @@ -477,15 +483,6 @@ end ### MEMORY SETTING PROCEDURES aka. global variables -proc mem_setw_recipient(recipient: word) - mem_storew_le.NOTE_RECIPIENT_WORD dropw -end -proc mem_set_note_tag(note_tag: felt) - mem_store.NOTE_TAG -end -proc mem_set_note_type(note_type: felt) - mem_store.NOTE_TYPE -end proc mem_set_user_id(user_id_prefix: felt, user_id_suffix: felt) mem_store.USER_ACCOUNT_ID_PREFIX mem_store.USER_ACCOUNT_ID_SUFFIX end @@ -502,21 +499,29 @@ proc mem_get_note_type() -> felt mem_load.NOTE_TYPE exec.sys::truncate_stack end -proc mem_get_recipient() -> word - padw mem_load.NOTE_RECIPIENT_WORD - exec.sys::truncate_stack -end + @locals(8) proc create_withdraw_return_note(amount_0_out: felt, amount_1_out: felt) -> felt # loc.0: amount_0_out # loc.1: amount_1_out - # loc.2: note_id # loc.3: asset_0_id_prefix # loc.4: asset_0_id_suffix loc_store.0 loc_store.1 - padw mem_loadw_le.NOTE_RECIPIENT_WORD mem_load.NOTE_TYPE mem_load.NOTE_TAG + ##### compute recipient hash + padw mem_loadw_le.OUTPUT_NOTE_ROOT_HASH exec.compute_return_note_serial_num + # => [WITHDRAW_SERIAL_NUM, OUTPUT_NOTE_ROOT_HASH] + + mem_load.USER_ACCOUNT_ID_SUFFIX mem_store.0 + mem_load.USER_ACCOUNT_ID_PREFIX mem_store.1 +# + push.2 push.0 + ## => [inputs_ptr, number_of_inputs, WITHDRAW_SERIAL_NUM, OUTPUT_NOTE_ROOT_HASH] + exec.note::build_recipient + # => [RECIPIENT] + mem_load.NOTE_TYPE mem_load.NOTE_TAG + # => [note_type, note_tag, RECIPIENT] exec.output_note::create # => [note_id] loc_store.2 @@ -528,7 +533,7 @@ proc create_withdraw_return_note(amount_0_out: felt, amount_1_out: felt) -> felt push.0 movdn.2 loc_load.1 movdn.3 # => [ASSET1_OUT] exec.native_account::remove_asset - # => [ASSET1_OUT] + # => [ASSET1_uOUT] loc_load.2 movdn.4 # => [ASSET1_OUT, note_id] exec.output_note::add_asset @@ -542,141 +547,13 @@ proc create_withdraw_return_note(amount_0_out: felt, amount_1_out: felt) -> felt loc_load.2 movdn.4 # => [ASSET0_OUT, note_id] exec.output_note::add_asset - # => [] end -# # => [amount_0_out, amount_1_out] -# loc_store.5 loc_store.6 -# ### check if initial deposit -# -# ### mint to user -# -# mem_load.USER_ACCOUNT_ID_SUFFIX mem_load.USER_ACCOUNT_ID_PREFIX -# loc_load.3 -# # => [lp_amount,user_id_prefix, user_id_suffix] -# exec.burn -# -# ### add assets back to user -# ### reserve needs to be updated -# #padw padw padw loc_loadw_be.0 exec.receive_asset -# #padw padw padw loc_loadw_be.4 exec.receive_asset -# #@todo - move to note valdiate -# #@todo - also simulate_withdraw a -# padw loc_loadw_le.4 exec.native_account::add_asset dropw -# padw loc_loadw_le.5 exec.native_account::add_asset dropw -# ## make sure asset order correct -# loc_load.7 loc_load.3 -# # => [amount_0, amount_1] -# exec.add_to_reserve -#end - -##! Inputs: [user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET_OUT, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage] -#pub proc withdraw -# dupw -# # => [user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET_OUT, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# dupw.2 -# # => [ASSET_OUT, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET_OUT, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# exec.get_user_deposit_key -# # => [KEY, lp_withdraw_amount, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0 ] -# mem_storew_be.WITHDRAW_KEY_LOC -# exec.get_user_deposit -# # => [user_lp_shares, lp_withdraw_amount, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# swap sub -# # => [new_user_lp_shares, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0 ] -# dup -# # => [new_user_lp_shares, new_user_lp_shares, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# padw mem_loadw_be.WITHDRAW_KEY_LOC -# exec.get_user_deposit -# # => [user_lp_shares, new_user_lp_shares, new_user_lp_shares, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# lt assert.err=ERR_LP_WITHDRAW_AMOUNT_EXCEEDS_USER_LP_SHARES -# # => [new_user_lp_shares, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# -# # store new user lp shares -# push.0.0.0 -# # => [NEW_USER_LP_SHARES, 0, user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# padw mem_loadw_be.WITHDRAW_KEY_LOC -# push.USER_DEPOSITS_MAPPING_SLOT[0..2] -# # => [slot_id_prefix, slot_id_suffix, KEY, NEW_USER_LP_SHARES] -# exec.native_account::set_map_item -# dropw drop -# # => [user_id_prefix, user_id_suffix, lp_withdraw_amount, 0, ASSET, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# drop drop swap drop -# mem_store.LP_SHARES_AMOUNT_ADDR -# -# -# mem_load.LP_SHARES_AMOUNT_ADDR push.0.0.0 -# padw mem_loadw_be.WITHDRAW_KEY_LOC -# exec.get_lp_shares_total_supply_key -# # => [KEY, SHARE_AMOUNT] -# push.USER_DEPOSITS_MAPPING_SLOT[0..2] -# # => [slot_id_prefix, slot_id_suffix, KEY, SHARE_AMOUNT] -# exec.sub_from_map_item -# # => [ ASSET_OUT, noteid, 0,0,0, liabilities, reserve, reserve_with_slippage, 0] -# exec.move_asset_to_note -# dropw dropw dropw -#end - - -##! Atomically increments a storage map entry: map[KEY] += VALUE. -##! Reads the current value via get_map_item, adds VALUE, writes back via set_map_item. -##! -##! Inputs: [slot_id_prefix, slot_id_suffix, KEY, VALUE] -##! Outputs: [] -#proc add_to_map_item -# # Save slot ID to memory -# mem_store.HELPER_SLOT_ID_PREFIX_LOC -# mem_store.HELPER_SLOT_ID_SUFFIX_LOC -# # => [KEY, VALUE] -# -# # Duplicate KEY for second call -# dupw -# # => [KEY, KEY, VALUE] -# -# # Load slot ID and get current map value -# mem_load.HELPER_SLOT_ID_SUFFIX_LOC -# mem_load.HELPER_SLOT_ID_PREFIX_LOC -# # => [slot_id_prefix, slot_id_suffix, KEY, KEY, VALUE] -# exec.active_account::get_map_item drop drop drop -# # => [old_amount, KEY, VALUE] -# -# # Add new amount to old amount -# movup.8 add -# # => [new_amount, KEY, 0, 0, 0] -# movdn.7 -# # => [KEY, 0, 0, 0, new_amount] -# # => [KEY, NEW_VALUE] -# -# # Load slot ID and set map item -# mem_load.HELPER_SLOT_ID_SUFFIX_LOC -# mem_load.HELPER_SLOT_ID_PREFIX_LOC -# # => [slot_id_prefix, slot_id_suffix, KEY, NEW_VALUE] -# exec.native_account::set_map_item dropw -# # => [] -#end - - -#! Adds the provided asset to the active account. -#! -#! Inputs: [ASSET, pad(4, pad(8)] -#! Outputs: [pad(16)] -#! -#! Where: -#! - ASSET is the asset to be received, can be fungible or non-fungible -#! -#! Panics if: -#! - the same non-fungible asset already exists in the account. -#! - adding a fungible asset would result in amount overflow, i.e., -#! the total amount would be greater than 2^63. -#! -#! Invocation: call -#pub proc receive_asset -# exec.native_account::add_asset -# # => [ASSET', reserve, reserve_with_slippage, liabilities, 0, pad(8)] -# -# procref.validate_and_update_state mem_storew_be.DYNAMIC_PROC_ADDR dropw push.DYNAMIC_PROC_ADDR -# dynexec -# -# # => [pad(16)] -#end +proc compute_return_note_serial_num() -> word + padw mem_loadw_le.INPUT_NOTE_SERIAL + # => [INPUT_NOTE_SERIAL] + add.1 + # => [SERIAL_NUM] +end diff --git a/src/masm/accounts/xyk_pool.masm b/src/masm/accounts/xyk_pool.masm index 24eafc8..6f72760 100644 --- a/src/masm/accounts/xyk_pool.masm +++ b/src/masm/accounts/xyk_pool.masm @@ -1,13 +1,15 @@ use miden::protocol::native_account #use miden::protocol::active_account -#use miden::protocol::active_note +use miden::protocol::active_note use miden::protocol::output_note #use miden::protocol::asset - use miden::core::sys - use miden::core::math::u64 +use miden::protocol::note +use miden::protocol::active_account +use miden::protocol::account_id + use zoro::math #use zoro::storage_utils @@ -47,14 +49,17 @@ const RETURN_NOTE_DETAILS_WORD = 32 const RETURN_NOTE_TAG = 32 const RETURN_NOTE_TYPE = 33 -const RETURN_NOTE_RECIPIENT_WORD = 36 +const P2ID_SCRIPT_ROOT = 36 const RETURN_NOTE_ASSET_WORD = 40 const RETURN_NOTE_ASSET_ID_PREFIX = 40 const RETURN_NOTE_ASSET_ID_SUFFIX = 41 const RETURN_NOTE_AMOUNT = 43 - +const USER_ACCOUNT_ID_WORD = 48 +const USER_ACCOUNT_ID_PREFIX = 48 +const USER_ACCOUNT_ID_SUFFIX = 49 +const INPUT_NOTE_SERIAL = 52 pub proc quote(amount_A: felt, reserve_A: felt, reserve_B: felt) -> felt # => [amount_A, reserve_A, reserve_B] @@ -165,15 +170,27 @@ end @locals(2) -pub proc swap_exact_tokens_for_tokens(asset_in: word, min_asset_out: word, deadline: felt, note_tag: felt, note_type: felt, return_note_recipient: word) -> felt - # loc.0: deadline - # loc.1: amount_out +pub proc swap_exact_tokens_for_tokens(note_serial: word, user_id_prefix: felt, user_id_suffix: felt, asset_in: word, min_asset_out: word, note_tag: felt, note_type: felt, p2id_script_root: word) -> felt + + # Initial stack + # => [sender_prefix, sender_suffix, ASSET_IN, MIN_ASSET_OUT_WORD, note_tag, note_type, P2ID_SCRIPT_ROOT] + + # get SERIAL of this note + exec.active_note::get_serial_number + # => [serial_num, sender_prefix, sender_suffix, ASSET_IN, MIN_ASSET_OUT_WORD, note_tag, note_type, P2ID_SCRIPT_ROOT] + + # store user account id + mem_storew_le.INPUT_NOTE_SERIAL dropw + + # store user account id + mem_store.USER_ACCOUNT_ID_PREFIX mem_store.USER_ACCOUNT_ID_SUFFIX + ### mem store asset in, min asset out - mem_storew_le.ASSET_IN_WORD dropw mem_storew_le.ASSET_OUT_WORD dropw - ### localy store deadline - loc_store.0 + mem_storew_le.ASSET_IN_WORD dropw mem_storew_le.ASSET_OUT_WORD dropw + ### store note metadata in memory - mem_store.RETURN_NOTE_TAG mem_store.RETURN_NOTE_TYPE mem_storew_le.RETURN_NOTE_RECIPIENT_WORD dropw + mem_store.RETURN_NOTE_TAG mem_store.RETURN_NOTE_TYPE mem_storew_le.P2ID_SCRIPT_ROOT dropw + mem_load.ASSET_OUT_ID_SUFFIX mem_load.ASSET_OUT_ID_PREFIX exec.get_reserve_by_asset_id # => [reserve_out] @@ -219,16 +236,27 @@ pub proc swap_exact_tokens_for_tokens(asset_in: word, min_asset_out: word, deadl end @locals(3) -pub proc swap_tokens_for_exact_tokens(asset_out: word, max_asset_in: word, deadline: felt, note_tag: felt, note_type: felt, return_note_recipient: word) -> felt - # loc.0: deadline - # loc.1: amount_in - # loc.2: remaining_amount_in - ### mem store asset out, max asset in - mem_storew_le.ASSET_OUT_WORD dropw mem_storew_le.ASSET_IN_WORD dropw - ### localy store deadline - loc_store.0 +pub proc swap_tokens_for_exact_tokens(user_id_prefix: felt, user_id_suffix: felt, asset_out: word, max_asset_in: word, note_tag: felt, note_type: felt, p2id_script_root: word) -> felt + + # Initial stack + # => [sender_prefix, sender_suffix, ASSET_IN, MIN_ASSET_OUT_WORD, note_tag, note_type, P2ID_SCRIPT_ROOT] + + # get SERIAL of this note + exec.active_note::get_serial_number + # => [serial_num, sender_prefix, sender_suffix, ASSET_IN, MIN_ASSET_OUT_WORD, note_tag, note_type, P2ID_SCRIPT_ROOT] + + # store user account id + mem_storew_le.INPUT_NOTE_SERIAL dropw + + # store user account id + mem_store.USER_ACCOUNT_ID_PREFIX mem_store.USER_ACCOUNT_ID_SUFFIX + + ### mem store asset in, min asset out + mem_storew_le.ASSET_OUT_WORD dropw mem_storew_le.ASSET_IN_WORD dropw + ### store note metadata in memory - mem_store.RETURN_NOTE_TAG mem_store.RETURN_NOTE_TYPE mem_storew_le.RETURN_NOTE_RECIPIENT_WORD dropw + mem_store.RETURN_NOTE_TAG mem_store.RETURN_NOTE_TYPE mem_storew_le.P2ID_SCRIPT_ROOT dropw + mem_load.ASSET_OUT_ID_SUFFIX mem_load.ASSET_OUT_ID_PREFIX exec.get_reserve_by_asset_id # => [reserve_out] @@ -286,12 +314,30 @@ pub proc swap_tokens_for_exact_tokens(asset_out: word, max_asset_in: word, deadl drop end +proc compute_return_note_serial_num() -> word + padw mem_loadw_le.INPUT_NOTE_SERIAL + # => [INPUT_NOTE_SERIAL] + add.1 + # => [SERIAL_NUM] +end #### HELPER PROCEDURES @locals(1) proc _create_p2id_note() -> felt - # loc.0: note_id - padw mem_loadw_le.RETURN_NOTE_RECIPIENT_WORD mem_load.RETURN_NOTE_TYPE mem_load.RETURN_NOTE_TAG + padw mem_loadw_le.P2ID_SCRIPT_ROOT + + exec.compute_return_note_serial_num + mem_load.USER_ACCOUNT_ID_SUFFIX mem_store.0 + mem_load.USER_ACCOUNT_ID_PREFIX mem_store.1 + push.2 push.0 + + ## => [inputs_ptr, number_of_inputs, RETURN_SERIAL_NUM, P2ID_SCRIPT_ROOT] + exec.note::build_recipient + # => [RECIPIENT] + + mem_load.RETURN_NOTE_TYPE mem_load.RETURN_NOTE_TAG + # => [note_type, note_tag, RECIPIENT] + exec.output_note::create loc_store.0 # => [] diff --git a/src/masm/notes/xyk_swap_exact_tokens_for_tokens.masm b/src/masm/notes/xyk_swap_exact_tokens_for_tokens.masm index b45e8bf..1314066 100644 --- a/src/masm/notes/xyk_swap_exact_tokens_for_tokens.masm +++ b/src/masm/notes/xyk_swap_exact_tokens_for_tokens.masm @@ -8,36 +8,40 @@ const ERR_INVALID_NUM_ASSETS = "Expected exactly 1 asset for swap" #! Note inputs layout (12 felts stored at dest_ptr=0): #! mem[0..3] = [0, 0, 0, min_amount_out] → MIN_ASSET_OUT word #! mem[4..7] = [deadline, note_tag, note_type, 0] -#! mem[8..11] = [r0, r1, r2, r3] → RECIPIENT digest +#! mem[8..11] = [r0, r1, r2, r3] → P2ID_SCRIPT_ROOT begin exec.active_note::get_inputs # => [num_inputs, dest_ptr] drop drop - # Push RECIPIENT word (mem[8..11]) + # Push P2ID_SCRIPT_ROOT word (mem[8..11]) padw mem_loadw_be.8 - # => [RECIPIENT_WORD] - - # Push note_type, note_tag, deadline - mem_load.6 mem_load.5 mem_load.4 - # => [deadline, note_tag, note_type, RECIPIENT_WORD] + # => [P2ID_SCRIPT_ROOT] + # Push note_type, note_tag and (mem_load.4 is deadline but then we run out of space if we use it) + mem_load.6 mem_load.5 + # => [note_tag, note_type, P2ID_SCRIPT_ROOT] + # Push MIN_ASSET_OUT word (mem[0..3]) padw mem_loadw_le.0 - # => [MIN_ASSET_OUT_WORD, deadline, note_tag, note_type, RECIPIENT_WORD] + # => [MIN_ASSET_OUT_WORD, note_tag, note_type, P2ID_SCRIPT_ROOT] # Load note asset to memory at ptr=40 push.40 exec.active_note::get_assets # => [num_assets, 40, ...] swap drop push.EXPECTED_NUM_ASSETS eq assert.err=ERR_INVALID_NUM_ASSETS + # => [MIN_ASSET_OUT_WORD, deadline, note_tag, note_type, P2ID_SCRIPT_ROOT] # Load ASSET_IN word padw mem_loadw_be.40 - # => [ASSET_IN, MIN_ASSET_OUT_WORD, deadline, note_tag, note_type, RECIPIENT_WORD] + # => [ASSET_IN, MIN_ASSET_OUT_WORD, note_tag, note_type, P2ID_SCRIPT_ROOT] + + # load user_id (sender) + exec.active_note::get_sender + # => [sender_prefix, sender_suffix, ASSET_IN, MIN_ASSET_OUT_WORD, note_tag, note_type, P2ID_SCRIPT_ROOT] call.xyk_pool::swap_exact_tokens_for_tokens exec.sys::truncate_stack end - diff --git a/src/masm/notes/xyk_swap_tokens_for_exact_tokens.masm b/src/masm/notes/xyk_swap_tokens_for_exact_tokens.masm index 1adb27b..472aff0 100644 --- a/src/masm/notes/xyk_swap_tokens_for_exact_tokens.masm +++ b/src/masm/notes/xyk_swap_tokens_for_exact_tokens.masm @@ -8,7 +8,7 @@ const ERR_INVALID_NUM_ASSETS = "Expected exactly 1 asset for swap" #! Note inputs layout (12 felts stored at dest_ptr=0): #! mem[0..3] = [aset_out_prefix, aset_out_suffix, 0, amount_out] → ASSET_OUT word #! mem[4..7] = [deadline, note_tag, note_type, 0] -#! mem[8..11] = [r0, r1, r2, r3] → RECIPIENT digest +#! mem[8..11] = [r0, r1, r2, r3] → P2ID_SCRIPT_ROOT begin exec.active_note::get_inputs @@ -17,15 +17,16 @@ begin # Push RECIPIENT word (mem[8..11]) padw mem_loadw_be.8 - # => [RECIPIENT_WORD] + # => [P2ID_SCRIPT_ROOT] - # Push note_type, note_tag, deadline - mem_load.6 mem_load.5 mem_load.4 - # => [deadline, note_tag, note_type, RECIPIENT_WORD] + # Push note_type, note_tag + # deadline we ignore, no space + mem_load.6 mem_load.5 + # => [deadline, note_tag, note_type, P2ID_SCRIPT_ROOT] # Push ASSET_OUT word (mem[0..3]) padw mem_loadw_le.0 - # => [ASSET_OUT_WORD, deadline, note_tag, note_type, RECIPIENT_WORD] + # => [ASSET_OUT_WORD, deadline, note_tag, note_type, P2ID_SCRIPT_ROOT] # Load note asset to memory at ptr=40 push.40 exec.active_note::get_assets @@ -35,11 +36,14 @@ begin # Load ASSET_IN word padw mem_loadw_be.40 - # => [MAX_ASSET_IN, ASSET_OUT, deadline, note_tag, note_type, RECIPIENT] + # => [MAX_ASSET_IN, ASSET_OUT, note_tag, note_type, P2ID_SCRIPT_ROOT] swapw - # => [ASSET_OUT, MAX_ASSET_IN, deadline, note_tag, note_type, RECIPIENT] + # => [ASSET_OUT, MAX_ASSET_IN, note_tag, note_type, P2ID_SCRIPT_ROOT] + + # load user_id (sender) + exec.active_note::get_sender + # => [sender_prefix, sender_suffix, ASSET_IN, MIN_ASSET_OUT_WORD, note_tag, note_type, P2ID_SCRIPT_ROOT] call.xyk_pool::swap_tokens_for_exact_tokens exec.sys::truncate_stack end - diff --git a/src/masm/notes/xyk_withdraw.masm b/src/masm/notes/xyk_withdraw.masm index 67b83da..b8e45a8 100644 --- a/src/masm/notes/xyk_withdraw.masm +++ b/src/masm/notes/xyk_withdraw.masm @@ -13,24 +13,23 @@ begin # note_inputs layout (dest_ptr = 0): # mem[0] = [lp_amount, 0, 0, 0] (word 0) # mem[4] = [note_tag, note_type, 0, 0] (word 1) - # mem[8] = [r0, r1, r2, r3] (word 2 - recipient digest) + # mem[8] = [OUTPUT_NOTE_ROOT_HASH] (word 2) - # Push recipient word (word 2) + # get SERIAL of this note + exec.active_note::get_serial_number + # => [SERIAL_NUM] + # load OUTPUT NOTE ROOT HASH padw mem_loadw_be.8 - # => [RECIPIENT_WORD] - - # Push note_type, note_tag + # => [OUTPUT_NOTE_ROOT_HASH, SERIAL_NUM] + # load note_type and note_tag mem_load.5 mem_load.4 - # => [note_tag, note_type, RECIPIENT_WORD] - - # Push user_id (sender) + # => [note_tag, note_type, OUTPUT_NOTE_ROOT_HASH, SERIAL_NUM] + # load user_id (sender) exec.active_note::get_sender - # => [sender_prefix, sender_suffix, note_tag, note_type, RECIPIENT_WORD] - - # Push LP_AMOUNT as a word [0, 0, 0, lp_amount] + # => [sender_prefix, sender_suffix, note_tag, note_type, OUTPUT_NOTE_ROOT_HASH] + # load LP_AMOUNT as a word [0, 0, 0, lp_amount] padw mem_loadw_le.0 - # => [LP_AMOUNT_WORD, sender_prefix, sender_suffix, note_tag, note_type, RECIPIENT_WORD] - + # => [LP_AMOUNT_WORD, sender_prefix, sender_suffix, note_tag, note_type, OUTPUT_NOTE_ROOT_HASH, SERIAL_NUM] call.lp_local::withdraw exec.sys::truncate_stack end diff --git a/src/pages/XykPoolDetail.tsx b/src/pages/XykPoolDetail.tsx index 148f151..b12bf83 100644 --- a/src/pages/XykPoolDetail.tsx +++ b/src/pages/XykPoolDetail.tsx @@ -5,28 +5,32 @@ import { PoolDetailHeader } from '@/components/PoolDetailHeader'; import { PoolDetailLayout } from '@/components/PoolDetailLayout'; import { PoolDetailStats } from '@/components/PoolDetailStats'; import { PoolInfoCard } from '@/components/PoolInfoCard'; -import { RecentTransactionsCard } from '@/components/RecentTransactionsCard'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { XykPoolModal } from '@/components/XykPoolModal'; +import { useBalance } from '@/hooks/useBalance'; import { useXykLpBalance } from '@/hooks/useXykLpBalance'; import { useXykPool } from '@/hooks/useXykPool'; +import type { XykTokenInfo } from '@/hooks/useXykPool'; import { useXykPoolNotes } from '@/hooks/useXykPoolNotes'; import { useXykSwap } from '@/hooks/useXykSwap'; -import { useBalance } from '@/hooks/useBalance'; -import { fullNumberBigintFormat, formatTokenAmountForInput, prettyBigintFormat } from '@/lib/format'; +import { + formatTokenAmountForInput, + fullNumberBigintFormat, + prettyBigintFormat, +} from '@/lib/format'; +import { getAmountOut } from '@/lib/xykMath'; import { getMockRecentTransactions } from '@/mocks/poolDetailMocks'; import { ModalContext } from '@/providers/ModalContext'; -import type { XykTokenInfo } from '@/hooks/useXykPool'; -import { getAmountOut } from '@/lib/xykMath'; +import { XykPoolDetailSkeleton } from '@/pages/skeletons/XykPoolDetailSkeleton'; import type { TokenConfig } from '@/providers/ZoroProvider'; +import { ArrowDownUp, Loader2 } from 'lucide-react'; import { useCallback, useContext, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { ArrowDownUp, Loader2 } from 'lucide-react'; import { parseUnits } from 'viem'; -const feeTierForSymbol = (_symbol: string) => '0.30%'; +const feeTierForSymbol = () => '0.30%'; export default function XykPoolDetail() { const { poolId } = useParams<{ poolId: string }>(); @@ -36,11 +40,10 @@ export default function XykPoolDetail() { decodedPoolId, ); const { lpBalance, refetch: refetchLpBalance } = useXykLpBalance(decodedPoolId); - const { notes: poolNotes, isLoading: notesLoading, error: notesError } = - useXykPoolNotes( - decodedPoolId, - poolData ?? null, - ); + const { notes: poolNotes } = useXykPoolNotes( + decodedPoolId, + poolData ?? null, + ); const { swap, isLoading: isSwapLoading } = useXykSwap(decodedPoolId); const [swapSellSide, setSwapSellSide] = useState<0 | 1>(0); const [amountInStr, setAmountInStr] = useState(''); @@ -90,10 +93,9 @@ export default function XykPoolDetail() { const expectedAmountOut = useMemo(() => { if (!poolData || !swapSellToken || !swapBuyToken || amountInBigint <= 0n) return 0n; - const [reserveIn, reserveOut] = - swapSellSide === 0 - ? [poolData.reserve0, poolData.reserve1] - : [poolData.reserve1, poolData.reserve0]; + const [reserveIn, reserveOut] = swapSellSide === 0 + ? [poolData.reserve0, poolData.reserve1] + : [poolData.reserve1, poolData.reserve0]; return getAmountOut(amountInBigint, reserveIn, reserveOut); }, [poolData, swapSellSide, swapSellToken, swapBuyToken, amountInBigint]); @@ -101,9 +103,9 @@ export default function XykPoolDetail() { () => swapBuyToken && expectedAmountOut >= 0n ? formatTokenAmountForInput({ - value: expectedAmountOut, - expo: swapBuyToken.decimals, - }) + value: expectedAmountOut, + expo: swapBuyToken.decimals, + }) : '', [swapBuyToken, expectedAmountOut], ); @@ -169,11 +171,7 @@ export default function XykPoolDetail() { }, [decodedPoolId, poolData]); if (poolLoading && !poolData) { - return ( - -

Loading pool…

-
- ); + return ; } if (poolError || !poolData) { @@ -254,6 +252,41 @@ export default function XykPoolDetail() { decimals0={poolData.token0.decimals} decimals1={poolData.token1.decimals} /> + + + {prettyBigintFormat({ + value: poolData.reserve0, + expo: poolData.token0.decimals, + })} + + ), + }, + { + label: `${poolData.token1.symbol} Reserve`, + value: ( + + + {prettyBigintFormat({ + value: poolData.reserve1, + expo: poolData.token1.decimals, + })} + + ), + }, + ]} + /> + +
+ +
Swap @@ -264,12 +297,10 @@ export default function XykPoolDetail() { From {swapSellToken && sellBalance != null && ( - Balance:{' '} - {prettyBigintFormat({ + Balance: {prettyBigintFormat({ value: sellBalance, expo: swapSellToken.decimals, - })}{' '} - {swapSellToken.symbol} + })} {swapSellToken.symbol} )}
@@ -321,12 +352,10 @@ export default function XykPoolDetail() { To {swapBuyToken && buyBalance != null && ( - Balance:{' '} - {prettyBigintFormat({ + Balance: {prettyBigintFormat({ value: buyBalance, expo: swapBuyToken.decimals, - })}{' '} - {swapBuyToken.symbol} + })} {swapBuyToken.symbol} )}
@@ -346,60 +375,26 @@ export default function XykPoolDetail() { - - - {prettyBigintFormat({ - value: poolData.reserve0, - expo: poolData.token0.decimals, - })} - - ), - }, - { - label: `${poolData.token1.symbol} Reserve`, - value: ( - - - {prettyBigintFormat({ - value: poolData.reserve1, - expo: poolData.token1.decimals, - })} - - ), - }, - ]} - /> - -
- -
- {/* + { + /* Pool notes @@ -478,7 +473,8 @@ export default function XykPoolDetail() { - */} + */ + } { /* +
+
+ + +
+ + +
+
+
+ + +
+
+
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + +
+ + + + +
+
+ +
+ + + +
+
+
+
+ + ); +} From 99a47b1da164da519f219c70859dddc49c196fa2 Mon Sep 17 00:00:00 2001 From: mico Date: Sat, 14 Mar 2026 21:54:15 +0100 Subject: [PATCH 25/49] Disclaimer --- src/App.tsx | 7 +- src/components/Disclaimer.tsx | 127 ++++++++++++++++++++++++++++++++++ src/components/FancyLogo.tsx | 24 +++++++ 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 src/components/Disclaimer.tsx create mode 100644 src/components/FancyLogo.tsx diff --git a/src/App.tsx b/src/App.tsx index 7a93eda..cd51ee8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ import HfPoolDetail from './pages/HfPoolDetail'; import XykPoolDetail from './pages/XykPoolDetail'; import NewXykPool from './pages/NewXykPool'; import Pools from './pages/Pools'; +import { DisclaimerGate } from './components/Disclaimer'; import ModalProvider from './providers/ModalProvider'; import { ParaProviderWrapper } from './providers/ParaProviderWrapper'; import { UnifiedWalletProvider } from './providers/UnifiedWalletProvider'; @@ -64,8 +65,9 @@ function App() { - - + + + diff --git a/src/components/Disclaimer.tsx b/src/components/Disclaimer.tsx new file mode 100644 index 0000000..850bd90 --- /dev/null +++ b/src/components/Disclaimer.tsx @@ -0,0 +1,127 @@ +import { FancyLogo } from '@/components/FancyLogo'; +import { poweredByMiden } from '@/components/PoweredByMiden'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import type { ReactNode } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; + +const DISCLAIMER_STORAGE_KEY = 'zoro-disclaimer-accepted'; + +function hasAcceptedDisclaimer(): boolean { + if (typeof window === 'undefined') return true; + return window.localStorage.getItem(DISCLAIMER_STORAGE_KEY) === 'true'; +} + +function acceptDisclaimer(): void { + if (typeof window === 'undefined') return; + window.localStorage.setItem(DISCLAIMER_STORAGE_KEY, 'true'); +} + +interface DisclaimerModalProps { + onAccept: () => void; +} + +function DisclaimerModal({ onAccept }: DisclaimerModalProps) { + const [visible, setVisible] = useState(false); + + useEffect(() => { + const t = requestAnimationFrame(() => { + requestAnimationFrame(() => setVisible(true)); + }); + return () => cancelAnimationFrame(t); + }, []); + + const handleAccept = useCallback(() => { + setVisible(false); + acceptDisclaimer(); + setTimeout(onAccept, 200); + }, [onAccept]); + + return createPortal( +
+ e.stopPropagation()} + > + + +
+ + Open Alpha + + + ZoroSwap is under active development — features and interfaces may change + without notice, and you may run into bugs. + +
+
+ +

+ The app runs on the Miden testnet. All tokens and assets here are for testing + only and have no monetary value. +

+ +
+ {poweredByMiden} +
+
+
+
, + document.body, + ); +} + +/** + * Renders the disclaimer modal when the user has not yet accepted it (once per + * device via localStorage). Mount once at app root (e.g. inside ModalProvider). + */ +export function DisclaimerGate({ children }: { children: ReactNode }) { + const [showDisclaimer, setShowDisclaimer] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) return; + // eslint-disable-next-line react-hooks/set-state-in-effect + setShowDisclaimer(!hasAcceptedDisclaimer()); + }, [mounted]); + + const handleAccept = useCallback(() => { + setShowDisclaimer(false); + }, []); + + return ( + <> + {children} + {showDisclaimer && } + + ); +} diff --git a/src/components/FancyLogo.tsx b/src/components/FancyLogo.tsx new file mode 100644 index 0000000..16da960 --- /dev/null +++ b/src/components/FancyLogo.tsx @@ -0,0 +1,24 @@ +import { cn } from '@/lib/utils'; + +interface FancyLogoProps { + className?: string; + size?: number; +} + +export function FancyLogo({ className, size = 56 }: FancyLogoProps) { + return ( +
+ ZoroSwap +
+ ); +} From 50069738d967a7af9df23f81dcc24177363bbec8 Mon Sep 17 00:00:00 2001 From: mico Date: Sun, 15 Mar 2026 11:49:18 +0100 Subject: [PATCH 26/49] LiquidityPools -> Explore --- docs/PERFORMANCE_EXPLORE.md | 33 +++++++++++++++++++ src/App.tsx | 7 ++-- src/pages/{LiquidityPools.tsx => Explore.tsx} | 4 +-- src/pages/HfPoolDetail.tsx | 6 ++-- 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 docs/PERFORMANCE_EXPLORE.md rename src/pages/{LiquidityPools.tsx => Explore.tsx} (99%) diff --git a/docs/PERFORMANCE_EXPLORE.md b/docs/PERFORMANCE_EXPLORE.md new file mode 100644 index 0000000..912e4ba --- /dev/null +++ b/docs/PERFORMANCE_EXPLORE.md @@ -0,0 +1,33 @@ +# Explore Page Performance Analysis + +## Symptom +The Explore page (formerly LiquidityPools) was laggy: CSS reactivity suffered (hovers, transitions), suggesting the main thread was busy and not able to process input/paint in time. + +## Root cause: constant re-renders from WebSocket polling + +**Primary culprit: `useWebSocket` (used by `useOrderUpdates` on Explore)** + +- The hook ran `setInterval(..., 1000)` and called `setIsConnected(ws.isConnected())` every second. +- That triggered a React state update every second, so every consumer of `useWebSocket` (and thus the whole Explore page via `useOrderUpdates`) re-rendered once per second. +- Frequent re-renders keep the main thread busy (reconciling the tree, running effects, etc.) and can make the UI feel frozen: hovers and animations are delayed or dropped because the browser is busy with React work. + +**Fix applied:** Only call `setIsConnected` when the value actually changes (store previous value in a ref and compare). The interval was also increased to 2s. This removes the constant re-renders; we now re-render at most when the connection actually connects or disconnects. + +## Other contributors (lower impact) + +1. **ZoroProvider `refreshPendingNotes` every 3s** + For Para wallet users, this runs every 3 seconds and can cause provider (and subtree) re-renders. Consider only running when the Explore page is focused or when `accountId` has pending notes. + +2. **useLPBalances `refetch` every 10s** + Explore refetches LP balances every 10s. That’s reasonable; if needed, increase to 30s or only refetch when the tab is visible (`document.visibilityState`). + +3. **LiquidityPoolsTable** + Renders one row per pool with no virtualization. For a large number of pools (e.g. 50+), consider virtualizing the list (e.g. `react-window` or `@tanstack/react-virtual`) so only visible rows are in the DOM. + +4. **Context and referential stability** + `ZoroProvider`’s value is memoized but depends on `poolsInfo`. If the pools query refetches and returns a new object reference, all context consumers re-render. Keeping `poolsInfo` referentially stable (e.g. in react-query with `structuralSharing`) helps. + +## Recommendations +- Prefer event-driven updates over polling where possible (e.g. WebSocket connection state via a callback from the WS client). +- For any remaining intervals, only call `setState` when the value actually changes. +- Consider virtualizing long pool lists if the number of pools grows. diff --git a/src/App.tsx b/src/App.tsx index cd51ee8..72627b1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { WalletProvider, } from '@demox-labs/miden-wallet-adapter'; import { useMemo } from 'react'; -import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import NotFound from './pages/404'; import FaucetPage from './pages/Faucet'; import SwapPage from './pages/Swap'; @@ -14,7 +14,7 @@ import '@demox-labs/miden-wallet-adapter-reactui/styles.css'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Bounce, ToastContainer } from 'react-toastify'; import Launchpad from './pages/Launchpad'; -import LiquidityPools from './pages/LiquidityPools'; +import Explore from './pages/Explore'; import HfPoolDetail from './pages/HfPoolDetail'; import XykPoolDetail from './pages/XykPoolDetail'; import NewXykPool from './pages/NewXykPool'; @@ -34,10 +34,9 @@ function AppRouter() { } /> } /> } /> - } /> + } /> } /> } /> - } /> } /> } /> } /> diff --git a/src/pages/LiquidityPools.tsx b/src/pages/Explore.tsx similarity index 99% rename from src/pages/LiquidityPools.tsx rename to src/pages/Explore.tsx index cd137c8..b195198 100644 --- a/src/pages/LiquidityPools.tsx +++ b/src/pages/Explore.tsx @@ -17,7 +17,7 @@ import { ZoroContext } from '@/providers/ZoroContext'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -function LiquidityPools() { +function Explore() { const navigate = useNavigate(); const { data: poolsInfo, refetch: refetchPoolsInfo, isLoading: isLoadingPools } = usePoolsInfo(); @@ -195,4 +195,4 @@ function LiquidityPools() { ); } -export default LiquidityPools; +export default Explore; diff --git a/src/pages/HfPoolDetail.tsx b/src/pages/HfPoolDetail.tsx index e9132a5..f68fbbd 100644 --- a/src/pages/HfPoolDetail.tsx +++ b/src/pages/HfPoolDetail.tsx @@ -99,9 +99,9 @@ export default function HfPoolDetail() { if (!pool || !poolBalance) { return ( - +

Pool not found.

- + ← Back to pools
@@ -119,7 +119,7 @@ export default function HfPoolDetail() { return ( From 2f075e8a47d5a7eef11a8e393a974d5272a04d3d Mon Sep 17 00:00:00 2001 From: mico Date: Sun, 15 Mar 2026 11:49:51 +0100 Subject: [PATCH 27/49] mend LiquidityPools -> Explore --- docs/PERFORMANCE_EXPLORE.md | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 docs/PERFORMANCE_EXPLORE.md diff --git a/docs/PERFORMANCE_EXPLORE.md b/docs/PERFORMANCE_EXPLORE.md deleted file mode 100644 index 912e4ba..0000000 --- a/docs/PERFORMANCE_EXPLORE.md +++ /dev/null @@ -1,33 +0,0 @@ -# Explore Page Performance Analysis - -## Symptom -The Explore page (formerly LiquidityPools) was laggy: CSS reactivity suffered (hovers, transitions), suggesting the main thread was busy and not able to process input/paint in time. - -## Root cause: constant re-renders from WebSocket polling - -**Primary culprit: `useWebSocket` (used by `useOrderUpdates` on Explore)** - -- The hook ran `setInterval(..., 1000)` and called `setIsConnected(ws.isConnected())` every second. -- That triggered a React state update every second, so every consumer of `useWebSocket` (and thus the whole Explore page via `useOrderUpdates`) re-rendered once per second. -- Frequent re-renders keep the main thread busy (reconciling the tree, running effects, etc.) and can make the UI feel frozen: hovers and animations are delayed or dropped because the browser is busy with React work. - -**Fix applied:** Only call `setIsConnected` when the value actually changes (store previous value in a ref and compare). The interval was also increased to 2s. This removes the constant re-renders; we now re-render at most when the connection actually connects or disconnects. - -## Other contributors (lower impact) - -1. **ZoroProvider `refreshPendingNotes` every 3s** - For Para wallet users, this runs every 3 seconds and can cause provider (and subtree) re-renders. Consider only running when the Explore page is focused or when `accountId` has pending notes. - -2. **useLPBalances `refetch` every 10s** - Explore refetches LP balances every 10s. That’s reasonable; if needed, increase to 30s or only refetch when the tab is visible (`document.visibilityState`). - -3. **LiquidityPoolsTable** - Renders one row per pool with no virtualization. For a large number of pools (e.g. 50+), consider virtualizing the list (e.g. `react-window` or `@tanstack/react-virtual`) so only visible rows are in the DOM. - -4. **Context and referential stability** - `ZoroProvider`’s value is memoized but depends on `poolsInfo`. If the pools query refetches and returns a new object reference, all context consumers re-render. Keeping `poolsInfo` referentially stable (e.g. in react-query with `structuralSharing`) helps. - -## Recommendations -- Prefer event-driven updates over polling where possible (e.g. WebSocket connection state via a callback from the WS client). -- For any remaining intervals, only call `setState` when the value actually changes. -- Consider virtualizing long pool lists if the number of pools grows. From 344cde803fcc0b29d00bbe9204db2e119f4b9792 Mon Sep 17 00:00:00 2001 From: mico Date: Sun, 15 Mar 2026 14:06:27 +0100 Subject: [PATCH 28/49] Nicer waiting for notes --- src/components/ProgressBar.tsx | 80 ++++++++++++++ src/components/xyk-wizard/XykWizard.tsx | 51 ++++++--- src/hooks/useLaunchpad.ts | 4 + src/hooks/useWaitForNoteConsumed.ts | 57 ++++++++++ src/hooks/useXykSwap.ts | 18 +++- src/pages/Launchpad.tsx | 67 ++---------- src/pages/XykPoolDetail.tsx | 134 ++++++++++++++++++++++-- src/providers/ZoroContext.ts | 3 + src/providers/ZoroProvider.tsx | 35 +++++++ 9 files changed, 363 insertions(+), 86 deletions(-) create mode 100644 src/components/ProgressBar.tsx create mode 100644 src/hooks/useWaitForNoteConsumed.ts diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx new file mode 100644 index 0000000..161ece4 --- /dev/null +++ b/src/components/ProgressBar.tsx @@ -0,0 +1,80 @@ +import { CheckCircle2, Loader2 } from 'lucide-react'; + +export interface ProgressBarProps { + /** Step labels shown in order. */ + steps: readonly string[]; + /** 0-based index of the current step. When null, the bar is typically hidden by the parent. */ + currentStepIndex: number | null; + /** Optional title above the bar (e.g. "Progress"). */ + title?: string; + /** Optional className for the wrapper. */ + className?: string; +} + +export function ProgressBar({ + steps, + currentStepIndex, + title = 'Progress', + className = '', +}: ProgressBarProps) { + if (currentStepIndex === null) return null; + + return ( +
+

{title}

+
+
+
+
    + {steps.map((label, i) => { + const done = i < currentStepIndex; + const current = i === currentStepIndex; + return ( +
  • + {done ? ( + + ) : current ? ( + + ) : ( + + )} + + {label} + +
  • + ); + })} +
+
+ ); +} diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index e4fbc28..eebae89 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -1,3 +1,4 @@ +import { ProgressBar } from '@/components/ProgressBar'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { UnifiedWalletButton } from '@/components/UnifiedWalletButton'; @@ -117,6 +118,12 @@ function clearPersistedWizard() { const wizardSteps = [XykStep1, XykStep2, XykStep3, XykStep4]; +export const XYK_CREATE_STEPS = [ + 'Deploying pool', + 'Adding liquidity', + 'Finalizing', +] as const; + export { XykPairIcon } from '@/components/XykPairIcon'; export interface XykWizardForm { @@ -195,6 +202,7 @@ const XykWizard = () => { }, [canGoBackInWizard, step]); const [isCreating, setIsCreating] = useState(false); + const [createStep, setCreateStep] = useState(null); const [createError, setCreateError] = useState(null); const launchXykPool = useCallback( @@ -205,7 +213,9 @@ const XykWizard = () => { amount0: bigint; amount1: bigint; }, + options?: { onProgress?: (step: number) => void }, ): Promise => { + const onProgress = options?.onProgress; try { if (!client) { throw new Error('Client not initialized'); @@ -214,8 +224,10 @@ const XykWizard = () => { throw new Error('User not logged in'); } + onProgress?.(0); const { newPoolId } = await deployNewPool({ client, token0, token1 }); + onProgress?.(1); const { tx } = await compileXykDepositTransaction({ token0, token1, @@ -225,21 +237,14 @@ const XykWizard = () => { poolAccountId: newPoolId, client, }); - const txId = await requestTransaction({ + await requestTransaction({ type: TransactionType.Custom, payload: tx, }); + onProgress?.(2); await client.syncState(); - // For testing with public acc - // const consumeTx = new TransactionRequestBuilder().withInputNotes( - // new NoteAndArgsArray([new NoteAndArgs(note, null)]), - // ).build(); - // await client.submitNewTransaction(newPoolId, consumeTx); - // - await client.syncState(); - console.log('Deposited, tx of deposit: ', txId); return newPoolId; } catch (e) { console.error(e); @@ -281,13 +286,17 @@ const XykWizard = () => { setCreateError(null); setIsCreating(true); + setCreateStep(null); try { - const newPoolId = await launchXykPool({ - token0: form.tokenA, - token1: form.tokenB, - amount0: form.amountA, - amount1: form.amountB, - }); + const newPoolId = await launchXykPool( + { + token0: form.tokenA, + token1: form.tokenB, + amount0: form.amountA, + amount1: form.amountB, + }, + { onProgress: (s) => setCreateStep(s) }, + ); if (newPoolId == null) { setCreateError('Pool creation failed. Please try again.'); @@ -307,6 +316,7 @@ const XykWizard = () => { setCreateError(message); } finally { setIsCreating(false); + setCreateStep(null); } }, [form, tokensWithBalance, launchXykPool]); @@ -426,7 +436,7 @@ const XykWizard = () => {
{activeStep}
-
+
{step !== 2 && step < wizardSteps.length - 1 && (
{isSubmitting && launchStep !== null && ( -
-

Progress

-
-
-
-
    - {LAUNCH_STEPS.map((label, i) => { - const done = i < launchStep; - const current = i === launchStep; - return ( -
  • - {done - ? ( - - ) - : current - ? ( - - ) - : ( - - )} - - {label} - -
  • - ); - })} -
-
+ )} {error && ( diff --git a/src/pages/XykPoolDetail.tsx b/src/pages/XykPoolDetail.tsx index b12bf83..5bd954b 100644 --- a/src/pages/XykPoolDetail.tsx +++ b/src/pages/XykPoolDetail.tsx @@ -5,6 +5,7 @@ import { PoolDetailHeader } from '@/components/PoolDetailHeader'; import { PoolDetailLayout } from '@/components/PoolDetailLayout'; import { PoolDetailStats } from '@/components/PoolDetailStats'; import { PoolInfoCard } from '@/components/PoolInfoCard'; +import { ProgressBar } from '@/components/ProgressBar'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; @@ -14,7 +15,9 @@ import { useXykLpBalance } from '@/hooks/useXykLpBalance'; import { useXykPool } from '@/hooks/useXykPool'; import type { XykTokenInfo } from '@/hooks/useXykPool'; import { useXykPoolNotes } from '@/hooks/useXykPoolNotes'; +import { useWaitForNoteConsumed } from '@/hooks/useWaitForNoteConsumed'; import { useXykSwap } from '@/hooks/useXykSwap'; +import { getMidenscanNoteUrl, getMidenscanTxUrl } from '@/hooks/useLaunchpad'; import { formatTokenAmountForInput, fullNumberBigintFormat, @@ -25,13 +28,15 @@ import { getMockRecentTransactions } from '@/mocks/poolDetailMocks'; import { ModalContext } from '@/providers/ModalContext'; import { XykPoolDetailSkeleton } from '@/pages/skeletons/XykPoolDetailSkeleton'; import type { TokenConfig } from '@/providers/ZoroProvider'; -import { ArrowDownUp, Loader2 } from 'lucide-react'; -import { useCallback, useContext, useMemo, useState } from 'react'; +import { ArrowDownUp, ExternalLink, Loader2, ArrowRight } from 'lucide-react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { parseUnits } from 'viem'; const feeTierForSymbol = () => '0.30%'; +const SWAP_PROGRESS_STEPS = ['Creating note', 'Sending note', 'Waiting'] as const; + export default function XykPoolDetail() { const { poolId } = useParams<{ poolId: string }>(); const decodedPoolId = poolId ? decodeURIComponent(poolId) : undefined; @@ -44,12 +49,28 @@ export default function XykPoolDetail() { decodedPoolId, poolData ?? null, ); - const { swap, isLoading: isSwapLoading } = useXykSwap(decodedPoolId); + const { swap, isLoading: isSwapLoading, error: swapError, noteId: swapNoteId, txId: swapTxId } = useXykSwap(decodedPoolId); + const waitForNoteConsumed = useWaitForNoteConsumed({ timeoutMs: 60_000 }); const [swapSellSide, setSwapSellSide] = useState<0 | 1>(0); const [amountInStr, setAmountInStr] = useState(''); const [swapInputError, setSwapInputError] = useState(); + const [swapProgressStep, setSwapProgressStep] = useState(null); + const [lastTrade, setLastTrade] = useState<{ + noteId: string; + txId: string | undefined; + amountIn: bigint; + amountOut: bigint; + sellSymbol: string; + buySymbol: string; + sellDecimals: number; + buyDecimals: number; + } | null>(null); const hasPosition = lpBalance > BigInt(0); + useEffect(() => { + if (!isSwapLoading) setSwapProgressStep(null); + }, [isSwapLoading]); + const xykTokenToConfig = useCallback((t: XykTokenInfo): TokenConfig => ({ symbol: t.symbol, name: t.name ?? t.symbol, @@ -139,14 +160,31 @@ export default function XykPoolDetail() { setSwapInputError('Enter amount to sell'); return; } - await swap( + setSwapProgressStep(null); + const result = await swap( swapSellToken.faucetId, swapBuyToken.faucetId, amountInBigint, expectedAmountOut, + { + onProgress: (step) => setSwapProgressStep(step), + waitForNoteConsumed, + }, ); - setAmountInStr(''); - }, [poolData, swapSellToken, swapBuyToken, amountInBigint, expectedAmountOut, swap]); + if (result) { + setAmountInStr(''); + setLastTrade({ + noteId: result.noteId, + txId: result.txId, + amountIn: amountInBigint, + amountOut: expectedAmountOut, + sellSymbol: swapSellToken.symbol, + buySymbol: swapBuyToken.symbol, + sellDecimals: swapSellToken.decimals, + buyDecimals: swapBuyToken.decimals, + }); + } + }, [poolData, swapSellToken, swapBuyToken, amountInBigint, expectedAmountOut, swap, waitForNoteConsumed]); const openXykModal = useCallback( (mode: 'Deposit' | 'Withdraw') => { @@ -369,8 +407,8 @@ export default function XykPoolDetail() {
- {swapInputError && ( -

{swapInputError}

+ {(swapInputError || swapError) && ( +

{swapInputError ?? swapError}

)} + + {page + 1} / {totalPages} + + +
+
+ )} + +
+ ); +}; + +export default XykPoolTable; diff --git a/src/components/XykTable/XykPoolTableRow.tsx b/src/components/XykTable/XykPoolTableRow.tsx new file mode 100644 index 0000000..c50d8b2 --- /dev/null +++ b/src/components/XykTable/XykPoolTableRow.tsx @@ -0,0 +1,117 @@ +import type { XykPool } from '@/hooks/useXykPools'; +import { useXykPool } from '@/hooks/useXykPool'; +import { prettyBigintFormat } from '@/lib/format'; +import { accountIdToBech32 } from '@/lib/utils'; +import { Loader2 } from 'lucide-react'; +import { useMemo } from 'react'; +import AssetIcon from '../AssetIcon'; +import { useNavigate } from 'react-router-dom'; + +const truncateId = (bech32: string, head = 6, tail = 4) => + bech32.length <= head + tail ? bech32 : `${bech32.slice(0, head)}…${bech32.slice(-tail)}`; + +export interface XykPoolTableRowProps { + pool: XykPool; + /** Token0 symbol if known (e.g. from config); otherwise fallback to truncated id */ + token0Symbol?: string; + /** Token1 symbol if known */ + token1Symbol?: string; +} + +const XykPoolTableRow = ({ pool, token0Symbol, token1Symbol }: XykPoolTableRowProps) => { + const navigate = useNavigate(); + const poolIdBech32 = useMemo(() => accountIdToBech32(pool.xykPoolId), [pool.xykPoolId]); + const { data: poolData, isLoading } = useXykPool(poolIdBech32); + + const fallbackId0 = accountIdToBech32(pool.token0); + const fallbackId1 = accountIdToBech32(pool.token1); + + const sym0 = poolData?.token0.symbol ?? token0Symbol; + const sym1 = poolData?.token1.symbol ?? token1Symbol; + const label0 = sym0 ?? truncateId(fallbackId0); + const label1 = sym1 ?? truncateId(fallbackId1); + const pairLabel = `${label0} / ${label1}`; + + const reserve0Str = poolData + ? prettyBigintFormat({ value: poolData.reserve0, expo: poolData.token0.decimals }) + : '—'; + const reserve1Str = poolData + ? prettyBigintFormat({ value: poolData.reserve1, expo: poolData.token1.decimals }) + : '—'; + + const priceDisplay = poolData && poolData.priceToken0InToken1 > 0 + ? `1 ${poolData.token0.symbol} = ${poolData.priceToken0InToken1.toFixed(6)} ${poolData.token1.symbol}` + : '—'; + + const onOpen = () => { + navigate(`/pools/xyk/${encodeURIComponent(poolIdBech32)}`); + }; + + return ( + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onOpen(); + } + }} + > + +
+
+ + + + + + +
+
+ {pairLabel} + + XYK + + {isLoading && ( + + + + )} +
+
+ + + {isLoading && !poolData + ? + : ( + <> + {reserve0Str} + {sym0 ? {sym0} : null} + / + {reserve1Str} + {sym1 ? {sym1} : null} + + )} + + + {isLoading && !poolData + ? + : {priceDisplay}} + + + {isLoading && !poolData ? : '—'} + + + {isLoading && !poolData ? : '—'} + + + {isLoading && !poolData ? : '—'} + + + ); +}; + +export default XykPoolTableRow; diff --git a/src/hooks/useLPBalances.ts b/src/hooks/useLPBalances.ts index 0e072bd..0bf4c1e 100644 --- a/src/hooks/useLPBalances.ts +++ b/src/hooks/useLPBalances.ts @@ -1,4 +1,4 @@ -import { bech32ToAccountId, accountIdToBech32 } from '@/lib/utils'; +import { accountIdToBech32, bech32ToAccountId } from '@/lib/utils'; import { ZoroContext } from '@/providers/ZoroContext'; import type { TokenConfig } from '@/providers/ZoroProvider'; import { Felt, Word } from '@miden-sdk/miden-sdk'; @@ -12,12 +12,11 @@ export const useLPBalances = ({ tokens }: { tokens?: TokenConfig[] }) => { if (!poolAccountId || !rpcClient || !accountId || !tokens) return; const balances: Record = {}; // Clone poolAccountId since getAccountDetails() consumes the AccountId argument - const poolIdClone = bech32ToAccountId(accountIdToBech32(poolAccountId))!; - const fetched = await rpcClient.getAccountDetails(poolIdClone); + const fetched = await rpcClient.getAccountDetails(poolAccountId); const storage = fetched.account()?.storage(); for (const token of tokens) { const lp = storage?.getMapItem( - "zoroswap::user_deposits", + 'zoroswap::user_deposits', Word.newFromFelts([ new Felt(accountId.suffix().asInt()), new Felt(accountId.prefix().asInt()), diff --git a/src/hooks/useXykPools.tsx b/src/hooks/useXykPools.tsx index 19e8f50..3075736 100644 --- a/src/hooks/useXykPools.tsx +++ b/src/hooks/useXykPools.tsx @@ -30,9 +30,9 @@ export const useXykPools = () => { const keyword = Word.fromHex(key).toFelts(); const valueword = Word.fromHex(value).toFelts(); - const token0 = accountIdFromPrefixSuffix(keyword[0], keyword[1]); - const token1 = accountIdFromPrefixSuffix(keyword[2], keyword[3]); - const xykPoolId = accountIdFromPrefixSuffix(valueword[2], valueword[3]); + const token0 = accountIdFromPrefixSuffix(valueword[1], valueword[0]); + const token1 = accountIdFromPrefixSuffix(valueword[3], valueword[2]); + const xykPoolId = accountIdFromPrefixSuffix(keyword[1], keyword[0]); xykPools.push({ token0, token1, xykPoolId }); } diff --git a/src/pages/Explore.tsx b/src/pages/Explore.tsx index b195198..015a5b6 100644 --- a/src/pages/Explore.tsx +++ b/src/pages/Explore.tsx @@ -2,18 +2,22 @@ import { AllDropdown } from '@/components/AllDropdown'; import { Footer } from '@/components/Footer'; import { Header } from '@/components/Header'; import LiquidityPoolsTable from '@/components/LiquidityPoolsTable'; +import { poweredByMiden } from '@/components/PoweredByMiden'; +import XykPoolTable from '@/components/XykTable/XykPoolTable'; import { type LpDetails, OrderStatus, type TxResult } from '@/components/OrderStatus'; import PoolModal from '@/components/PoolModal'; import type { LpActionType } from '@/components/PoolModal'; import { PositionCard } from '@/components/PositionCard'; import { SelectPoolModal } from '@/components/SelectPoolModal'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; import { useLPBalances } from '@/hooks/useLPBalances'; import { usePoolsBalances } from '@/hooks/usePoolsBalances'; import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; import { useOrderUpdates } from '@/hooks/useWebSocket'; import { ModalContext } from '@/providers/ModalContext'; import { ZoroContext } from '@/providers/ZoroContext'; +import { Search } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -33,6 +37,7 @@ function Explore() { const [txResult, setTxResult] = useState(); const [lpDetails, setLpDetails] = useState(undefined); const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); + const [communityPoolsSearch, setCommunityPoolsSearch] = useState(''); const tokenConfigs = useMemo( () => poolsInfo?.liquidityPools?.map(p => tokens[p.faucetIdBech32]), @@ -158,15 +163,15 @@ function Explore() { )) : (
- No positions yet. Add liquidity in Existing Pools below. + No positions yet. Add liquidity in High frequency pools below.
)}
-
+

- Existing Pools + High frequency pools

+
+ {poweredByMiden} +
+

+ Community pools +

+
+ + setCommunityPoolsSearch(e.target.value)} + className='pl-9 rounded-lg bg-muted/50 border-muted-foreground/20' + /> +
+
+ ); } diff --git a/src/components/StatusBanner.tsx b/src/components/StatusBanner.tsx new file mode 100644 index 0000000..0bc409e --- /dev/null +++ b/src/components/StatusBanner.tsx @@ -0,0 +1,100 @@ +import { AlertTriangle, Info, X } from 'lucide-react'; +import { useCallback, useEffect, useState } from 'react'; + +const BANNER_URL = '/banner.md'; +const BANNER_POLL_MS = 180_000; + +type BannerLevel = 'info' | 'warning' | 'error'; + +interface ParsedBanner { + level: BannerLevel; + text: string; +} + +function parseBanner(raw: string): ParsedBanner | null { + const trimmed = raw.trim(); + if (!trimmed) return null; + + const lines = trimmed.split('\n'); + const firstLine = lines[0].trim(); + + let level: BannerLevel = 'info'; + let text = trimmed; + + // Support optional front-matter style: `level: warning` + const levelMatch = firstLine.match(/^level:\s*(info|warning|error)$/i); + if (levelMatch) { + level = levelMatch[1].toLowerCase() as BannerLevel; + text = lines.slice(1).join('\n').trim(); + } + + if (!text) return null; + return { level, text }; +} + +const levelStyles: Record = { + info: { + bg: 'bg-blue-500/10', + text: 'text-blue-700 dark:text-blue-300', + border: 'border-blue-500/20', + }, + warning: { + bg: 'bg-yellow-500/10', + text: 'text-yellow-700 dark:text-yellow-300', + border: 'border-yellow-500/20', + }, + error: { + bg: 'bg-red-500/10', + text: 'text-red-700 dark:text-red-300', + border: 'border-red-500/20', + }, +}; + +export function StatusBanner() { + const [banner, setBanner] = useState(null); + const [dismissed, setDismissed] = useState(false); + + const fetchBanner = useCallback(async () => { + try { + const res = await fetch(BANNER_URL, { cache: 'no-store' }); + if (!res.ok) { + setBanner(null); + return; + } + const text = await res.text(); + setBanner(parseBanner(text)); + } catch { + setBanner(null); + } + }, []); + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + fetchBanner(); + const id = setInterval(fetchBanner, BANNER_POLL_MS); + return () => clearInterval(id); + }, [fetchBanner]); + + if (!banner || dismissed) return null; + + const styles = levelStyles[banner.level]; + const Icon = banner.level === 'info' ? Info : AlertTriangle; + + return ( +
+
+ +

+ {banner.text} +

+ +
+
+ ); +} diff --git a/src/components/xyk-wizard/XykWizard.tsx b/src/components/xyk-wizard/XykWizard.tsx index c9aa648..173846d 100644 --- a/src/components/xyk-wizard/XykWizard.tsx +++ b/src/components/xyk-wizard/XykWizard.tsx @@ -4,6 +4,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { UnifiedWalletButton } from '@/components/UnifiedWalletButton'; import useTokensWithBalance from '@/hooks/useTokensWithBalance'; import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; +import { useXykPools } from '@/hooks/useXykPools'; import { deployNewPool, registerPool } from '@/lib/DeployXykPool'; import { accountIdToBech32 } from '@/lib/utils'; import { compileXykDepositTransaction } from '@/lib/XykDepositNote'; @@ -138,17 +139,58 @@ export interface XykStepProps { restart: () => void; /** Set after successful deploy; used by step 4 for "View pool" link. */ lastDeployedPoolIdBech32?: string; + /** bech32 IDs of hfAMM tokens — pairing two of these is forbidden. */ + hfAmmBech32s?: ReadonlySet; + /** Set of "tokenA|tokenB" keys (both orderings) for existing XYK pool pairs. */ + registeredPairs?: ReadonlySet; + /** Validation error for the current token pair selection. */ + pairError?: string; } const XykWizard = () => { const { connected, requestTransaction } = useUnifiedWallet(); - const { client, accountId } = useContext(ZoroContext); + const { client, accountId, tokens: hfAmmTokens } = useContext(ZoroContext); const [form, setForm] = useState(() => readPersistedWizard().form); const [step, setStep] = useState(() => readPersistedWizard().step); const [lastDeployedPoolIdBech32, setLastDeployedPoolIdBech32] = useState< string | undefined >(undefined); const tokensWithBalance = useTokensWithBalance(); + const { xykPools } = useXykPools(); + + const hfAmmBech32s = useMemo(() => { + const s = new Set(); + for (const t of Object.values(hfAmmTokens)) { + if (t.oracleId && t.oracleId !== '0x' && t.oracleId !== '') { + s.add(t.faucetIdBech32); + } + } + return s; + }, [hfAmmTokens]); + + const registeredPairs = useMemo(() => { + const s = new Set(); + for (const pool of xykPools) { + const t0 = accountIdToBech32(pool.token0); + const t1 = accountIdToBech32(pool.token1); + s.add(`${t0}|${t1}`); + s.add(`${t1}|${t0}`); + } + return s; + }, [xykPools]); + + const pairError = useMemo(() => { + if (!form.tokenA || !form.tokenB) return undefined; + const aBech = accountIdToBech32(form.tokenA); + const bBech = accountIdToBech32(form.tokenB); + if (hfAmmBech32s.has(aBech) && hfAmmBech32s.has(bBech)) { + return 'Two hfAMM tokens cannot be paired together. Use one hfAMM token with one non-hfAMM token.'; + } + if (registeredPairs.has(`${aBech}|${bBech}`)) { + return 'This pair already exists in the registry. You cannot create a duplicate pool.'; + } + return undefined; + }, [form.tokenA, form.tokenB, hfAmmBech32s, registeredPairs]); useEffect(() => { writePersistedWizard(step, form); @@ -169,7 +211,7 @@ const XykWizard = () => { switch (step) { case 0: return form.tokenA != null && form.tokenB != null && form.tokenA != form.tokenB - && form.feeBps != null && form.feeBps > 0; + && form.feeBps != null && form.feeBps > 0 && !pairError; case 1: return form.amountA != null && form.amountA > BigInt(0) && form.amountB != null && form.amountB > BigInt(0); @@ -178,7 +220,7 @@ const XykWizard = () => { default: return false; } - }, [step, form]); + }, [step, form, pairError]); const canGoBackInWizard = useMemo(() => step > 0 && step < wizardSteps.length - 1, [ step, @@ -336,9 +378,12 @@ const XykWizard = () => { loading={tokensWithBalance.loading} restart={restart} lastDeployedPoolIdBech32={lastDeployedPoolIdBech32} + hfAmmBech32s={hfAmmBech32s} + registeredPairs={registeredPairs} + pairError={pairError} /> ); - }, [step, form, tokensWithBalance, restart, lastDeployedPoolIdBech32]); + }, [step, form, tokensWithBalance, restart, lastDeployedPoolIdBech32, hfAmmBech32s, registeredPairs, pairError]); if (!connected) { return ( diff --git a/src/components/xyk-wizard/steps/XykWizardStep1.tsx b/src/components/xyk-wizard/steps/XykWizardStep1.tsx index 8c51875..89aee07 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep1.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep1.tsx @@ -1,7 +1,7 @@ import { TokenAutocomplete } from '@/components/TokenAutocomplete'; import { accountIdToBech32, cn } from '@/lib/utils'; import { AccountId } from '@miden-sdk/miden-sdk'; -import { ArrowRight, Info } from 'lucide-react'; +import { AlertCircle, ArrowRight, Info } from 'lucide-react'; import { useCallback, useMemo } from 'react'; import type { XykStepProps } from '../XykWizard'; @@ -20,12 +20,63 @@ const FEE_TIERS = [ ] as const; const XykStep1 = ( - { tokensWithBalance, tokenMetadata, form, setForm, loading }: XykStepProps, + { + tokensWithBalance, + tokenMetadata, + form, + setForm, + loading, + hfAmmBech32s, + registeredPairs, + pairError, + }: XykStepProps, ) => { const availableTokens = useMemo(() => { return Object.values(tokenMetadata ?? {}); }, [tokenMetadata]); + // For B: disable tokens that would form a duplicate pair with A, + // or other hfAMM tokens when A is hfAMM. + const disabledForB = useMemo(() => { + const disabled = new Set(); + if (!form.tokenA) return disabled; + const aBech = accountIdToBech32(form.tokenA); + if (hfAmmBech32s?.has(aBech)) { + for (const b of hfAmmBech32s) { + if (b !== aBech) disabled.add(b); + } + } + if (registeredPairs) { + for (const t of availableTokens) { + if (registeredPairs.has(`${aBech}|${t.faucetIdBech32}`)) { + disabled.add(t.faucetIdBech32); + } + } + } + return disabled; + }, [form.tokenA, hfAmmBech32s, registeredPairs, availableTokens]); + + // For A: disable tokens that would form a duplicate pair with B, + // or other hfAMM tokens when B is hfAMM. + const disabledForA = useMemo(() => { + const disabled = new Set(); + if (!form.tokenB) return disabled; + const bBech = accountIdToBech32(form.tokenB); + if (hfAmmBech32s?.has(bBech)) { + for (const a of hfAmmBech32s) { + if (a !== bBech) disabled.add(a); + } + } + if (registeredPairs) { + for (const t of availableTokens) { + if (registeredPairs.has(`${t.faucetIdBech32}|${bBech}`)) { + disabled.add(t.faucetIdBech32); + } + } + } + return disabled; + }, [form.tokenB, hfAmmBech32s, registeredPairs, availableTokens]); + const setToken = useCallback((which: 'a' | 'b', id: AccountId) => { setForm({ ...form, ...(which === 'a' ? { tokenA: id } : { tokenB: id }) }); }, [form, setForm]); @@ -69,45 +120,55 @@ const XykStep1 = (

) : ( -
-
- - setToken('a', AccountId.fromBech32(val))} - excludeFaucetIdBech32={form.tokenB - ? accountIdToBech32(form.tokenB) - : undefined} - placeholder='Select a token' - className='w-full' - /> -
- - - -
- - setToken('b', AccountId.fromBech32(val))} - excludeFaucetIdBech32={form.tokenA - ? accountIdToBech32(form.tokenA) - : undefined} - placeholder='Select a token' - className='w-full' - /> + <> +
+
+ + setToken('a', AccountId.fromBech32(val))} + excludeFaucetIdBech32={form.tokenB + ? accountIdToBech32(form.tokenB) + : undefined} + disabledBech32s={disabledForA} + placeholder='Select a token' + className='w-full' + /> +
+ + + +
+ + setToken('b', AccountId.fromBech32(val))} + excludeFaucetIdBech32={form.tokenA + ? accountIdToBech32(form.tokenA) + : undefined} + disabledBech32s={disabledForB} + placeholder='Select a token' + className='w-full' + /> +
-
+ {pairError && ( +
+ + {pairError} +
+ )} + )}
diff --git a/src/pages/HfPoolDetail.tsx b/src/pages/HfPoolDetail.tsx index e9132a5..c94d262 100644 --- a/src/pages/HfPoolDetail.tsx +++ b/src/pages/HfPoolDetail.tsx @@ -7,11 +7,13 @@ import { PoolDetailHeader } from '@/components/PoolDetailHeader'; import { PoolDetailLayout } from '@/components/PoolDetailLayout'; import { PoolDetailStats } from '@/components/PoolDetailStats'; import { PoolInfoCard } from '@/components/PoolInfoCard'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useLPBalances } from '@/hooks/useLPBalances'; import { usePoolsBalances } from '@/hooks/usePoolsBalances'; import { type PoolInfo, usePoolsInfo } from '@/hooks/usePoolsInfo'; import { useOrderUpdates } from '@/hooks/useWebSocket'; -import { fullNumberBigintFormat } from '@/lib/format'; +import { formatTokenAmount, fullNumberBigintFormat } from '@/lib/format'; import { ModalContext } from '@/providers/ModalContext'; import { ZoroContext } from '@/providers/ZoroContext'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; @@ -65,6 +67,11 @@ export default function HfPoolDetail() { const lpBalance = pool ? lpBalances[pool.faucetIdBech32] ?? BigInt(0) : BigInt(0); const hasPosition = lpBalance > BigInt(0); + const poolSharePct = useMemo(() => { + if (!pool || !poolBalance || !hasPosition || poolBalance.totalLiabilities === 0n) return null; + return (Number(lpBalance) / Number(poolBalance.totalLiabilities)) * 100; + }, [pool, poolBalance, lpBalance, hasPosition]); + const openOrderStatusModal = useCallback((noteId: string) => { lastShownNoteId.current = noteId; setIsSuccessModalOpen(true); @@ -144,6 +151,60 @@ export default function HfPoolDetail() {
+ {hasPosition && pool && ( + + + Your Position + + +
+ LP Balance + + {formatTokenAmount({ value: lpBalance, expo: decimals })} + +
+
+ Pool Share + + {poolSharePct != null + ? poolSharePct < 0.01 + ? `${poolSharePct.toFixed(6)}%` + : `${poolSharePct.toFixed(2)}%` + : '—'} + +
+
+
+ + + {pool.symbol} + + + {formatTokenAmount({ value: lpBalance, expo: decimals })} + +
+
+
+ + +
+
+
+ )} - {accountId && balanceSell !== null && balanceSell !== undefined - && ( - - )} + {accountId && selectedAssetSell && ( + balanceSell === null + ? ( + + + {selectedAssetSell.symbol} + + ) + : ( + + ) + )}
From 5983b82a42666e86944e969bf44b099899f46e19 Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 19 Mar 2026 17:10:50 +0100 Subject: [PATCH 44/49] Nicer launchpad --- src/pages/Launchpad.tsx | 167 ++++++++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 66 deletions(-) diff --git a/src/pages/Launchpad.tsx b/src/pages/Launchpad.tsx index 7858072..e0e25c5 100644 --- a/src/pages/Launchpad.tsx +++ b/src/pages/Launchpad.tsx @@ -1,10 +1,10 @@ import { Footer } from '@/components/Footer'; import { Header } from '@/components/Header'; +import { ProgressBar } from '@/components/ProgressBar'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { UnifiedWalletButton } from '@/components/UnifiedWalletButton'; -import { ProgressBar } from '@/components/ProgressBar'; import useLaunchpad, { getMidenscanAccountUrl, getMidenscanTxUrl, @@ -14,7 +14,7 @@ import useLaunchpad, { } from '@/hooks/useLaunchpad'; import { useUnifiedWallet } from '@/hooks/useUnifiedWallet'; import { truncateId } from '@/lib/format'; -import { ArrowLeft, CheckCircle, ExternalLink, Loader2 } from 'lucide-react'; +import { ArrowLeft, CheckCircle, ExternalLink, Info, Loader2 } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { parseUnits } from 'viem'; @@ -22,8 +22,10 @@ import { parseUnits } from 'viem'; const SYMBOL_MIN = 3; const SYMBOL_MAX = 6; const DECIMALS_MIN = 0; -const DECIMALS_MAX = 4; -const TOTAL_SUPPLY_MAX = 1_000_000; +const DECIMALS_MAX = 6; +const TOTAL_SUPPLY_MAX = 100_000_000; + +const MAX_SUPPLY_DISPLAY = TOTAL_SUPPLY_MAX.toLocaleString('en-US'); function validateSymbol(s: string): string | null { const trimmed = s.trim().toUpperCase(); @@ -55,11 +57,15 @@ function validateInitialSupply(raw: string, decimals: number): string | null { } if (amount <= 0n) return 'Initial supply must be greater than 0'; if (amount > TOTAL_SUPPLY_MAX) { - return `Initial supply should be lower than ${TOTAL_SUPPLY_MAX}`; + return `Initial supply must not exceed ${MAX_SUPPLY_DISPLAY} (raw units)`; } return null; } +const bodyClass = 'text-sm text-muted-foreground leading-relaxed'; +const labelClass = 'text-sm font-medium text-foreground'; +const hintClass = 'text-sm text-muted-foreground'; + export default function Launchpad() { const { connected } = useUnifiedWallet(); const { launchToken, error, clearError } = useLaunchpad(); @@ -150,51 +156,56 @@ export default function Launchpad() { Launchpad - ZoroSwap | DeFi on Miden
-
+
- + Back to Swap - - - - Token Launchpad +
+

+ Token launchpad +

+

+ Deploy a new faucet token on Miden and mint the initial supply to your + account. You’ll confirm the transaction in your wallet. +

+
+ + + + + Configure your token -

- Create a new faucet token and mint initial supply to your wallet. -

-

- Launching a new token can take a couple of seconds. Please wait until the - process completes. +

+ Choose a symbol, decimals, and how many whole tokens to mint at launch. + Launch usually takes a few seconds—keep this tab open until it finishes.

- + {successResult ? ( -
-
+
+
- Token launched successfully + Token launched successfully
-

- Claim the note with your token supply in your wallet to receive the - tokens. You can launch another token below. +

+ Claim the note in your wallet to receive your tokens. You can launch + another token using the form below when you’re ready.

-
-
- -
+
+
+ Faucet ID +
-
- -
+
+ Transaction ID +
@@ -236,7 +245,7 @@ export default function Launchpad() { href={getMidenscanTxUrl(successResult.txId)} target='_blank' rel='noopener noreferrer' - className='shrink-0 text-muted-foreground hover:text-foreground' + className='shrink-0 text-muted-foreground hover:text-foreground p-1' aria-label='View tx on MidenScan' > @@ -246,17 +255,16 @@ export default function Launchpad() { href={getMidenscanTxUrl(successResult.txId)} target='_blank' rel='noopener noreferrer' - className='inline-flex items-center justify-center gap-2 w-full rounded-lg border border-border py-2.5 text-sm font-medium hover:bg-muted/50' + className='inline-flex items-center justify-center gap-2 w-full rounded-xl border border-border py-2.5 text-sm font-medium hover:bg-muted/50 transition-colors' > View transaction on MidenScan
-
-

- Go to your{' '} - wallet and claim the pending note to receive your token supply in - your wallet. +

+

+ Open your wallet and claim the pending note so the minted supply + appears in your balance.

@@ -266,17 +274,17 @@ export default function Launchpad() { {!connected ? ( -
-

- Connect your wallet to launch a token. +

+

+ Connect your wallet to deploy a token on Miden testnet.

- +
) : ( -
+
-
-
-
{isSubmitting && launchStep !== null && ( @@ -362,7 +377,7 @@ export default function Launchpad() { {error && (
{error} @@ -371,7 +386,7 @@ export default function Launchpad() { )} + +
+ +

+ Supply & Miden math.{' '} + This launchpad caps initial mint at{' '} + + {MAX_SUPPLY_DISPLAY} + {' '} + raw units (before decimals). Because of current Miden field arithmetic + limits, tokens whose total supply (especially with many decimals) + grows beyond what the protocol can represent safely may show rounding + issues, failed operations, or other unexpected behaviour—even when below + this UI cap. Prefer conservative supplies and decimals until those limits + evolve. +

+
From 0e5ece891991f27bda2a811e5392c1d08c73070c Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 19 Mar 2026 17:32:15 +0100 Subject: [PATCH 45/49] Nicer launchpad 2, Manrope font for body --- index.html | 6 +++ src/components/XykTable/XykPoolTableRow.tsx | 31 +++++++++----- src/index.css | 2 + src/pages/Launchpad.tsx | 47 +++++++++++---------- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/index.html b/index.html index 7bc96c1..c1a5294 100644 --- a/index.html +++ b/index.html @@ -52,6 +52,12 @@ + + + diff --git a/src/components/XykTable/XykPoolTableRow.tsx b/src/components/XykTable/XykPoolTableRow.tsx index fe69a36..e7fa33d 100644 --- a/src/components/XykTable/XykPoolTableRow.tsx +++ b/src/components/XykTable/XykPoolTableRow.tsx @@ -1,15 +1,17 @@ -import type { XykPool } from '@/hooks/useXykPools'; import { useXykPool } from '@/hooks/useXykPool'; +import type { XykPool } from '@/hooks/useXykPools'; import { prettyBigintFormat } from '@/lib/format'; import { accountIdToBech32 } from '@/lib/utils'; import { Loader2 } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; -import AssetIcon from '../AssetIcon'; import { useNavigate } from 'react-router-dom'; +import AssetIcon from '../AssetIcon'; import { Skeleton } from '../ui/skeleton'; const truncateId = (bech32: string, head = 6, tail = 4) => - bech32.length <= head + tail ? bech32 : `${bech32.slice(0, head)}…${bech32.slice(-tail)}`; + bech32.length <= head + tail + ? bech32 + : `${bech32.slice(0, head)}…${bech32.slice(-tail)}`; export interface XykPoolTableRowProps { pool: XykPool; @@ -48,7 +50,9 @@ const XykPoolTableRow = ({ pool, token0Symbol, token1Symbol }: XykPoolTableRowPr : '—'; const priceDisplay = poolData && poolData.priceToken0InToken1 > 0 - ? `1 ${poolData.token0.symbol} = ${poolData.priceToken0InToken1.toFixed(6)} ${poolData.token1.symbol}` + ? `1 ${poolData.token0.symbol} = ${ + poolData.priceToken0InToken1.toFixed(6) + } ${poolData.token1.symbol}` : '—'; const onOpen = () => { @@ -83,9 +87,6 @@ const XykPoolTableRow = ({ pool, token0Symbol, token1Symbol }: XykPoolTableRowPr
{pairLabel} - - XYK - {isLoading && ( @@ -100,10 +101,14 @@ const XykPoolTableRow = ({ pool, token0Symbol, token1Symbol }: XykPoolTableRowPr : ( <> {reserve0Str} - {sym0 ? {sym0} : null} + {sym0 + ? {sym0} + : null} / {reserve1Str} - {sym1 ? {sym1} : null} + {sym1 + ? {sym1} + : null} )} @@ -116,10 +121,14 @@ const XykPoolTableRow = ({ pool, token0Symbol, token1Symbol }: XykPoolTableRowPr {isLoading && !poolData ? : '—'} - {isLoading && !poolData ? : '—'} + {isLoading && !poolData + ? + : '—'} - {isLoading && !poolData ? : '—'} + {isLoading && !poolData + ? + : '—'} ); diff --git a/src/index.css b/src/index.css index c4449ba..3739f77 100644 --- a/src/index.css +++ b/src/index.css @@ -95,6 +95,8 @@ body { @apply bg-background text-foreground; + font-family: 'Manrope', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + sans-serif; } } diff --git a/src/pages/Launchpad.tsx b/src/pages/Launchpad.tsx index e0e25c5..01e2f34 100644 --- a/src/pages/Launchpad.tsx +++ b/src/pages/Launchpad.tsx @@ -22,7 +22,7 @@ import { parseUnits } from 'viem'; const SYMBOL_MIN = 3; const SYMBOL_MAX = 6; const DECIMALS_MIN = 0; -const DECIMALS_MAX = 6; +const DECIMALS_MAX = 8; const TOTAL_SUPPLY_MAX = 100_000_000; const MAX_SUPPLY_DISPLAY = TOTAL_SUPPLY_MAX.toLocaleString('en-US'); @@ -48,16 +48,16 @@ function validateDecimals(n: number): string | null { } function validateInitialSupply(raw: string, decimals: number): string | null { - if (!raw.trim()) return 'Initial supply is required'; + if (!raw.trim()) return 'Total supply is required'; let amount: bigint; try { amount = parseUnits(raw.trim(), decimals); } catch { return 'Invalid amount'; } - if (amount <= 0n) return 'Initial supply must be greater than 0'; + if (amount <= 0n) return 'Total supply must be greater than 0'; if (amount > TOTAL_SUPPLY_MAX) { - return `Initial supply must not exceed ${MAX_SUPPLY_DISPLAY} (raw units)`; + return `Total supply must not exceed ${MAX_SUPPLY_DISPLAY} (raw units)`; } return null; } @@ -170,8 +170,8 @@ export default function Launchpad() { Token launchpad

- Deploy a new faucet token on Miden and mint the initial supply to your - account. You’ll confirm the transaction in your wallet. + Deploy a new faucet token on Miden and mint the whole initial supply to your + account. You will need to consume the tokens in your wallet.

@@ -191,7 +191,9 @@ export default function Launchpad() {
- Token launched successfully + + Token launched successfully +

Claim the note in your wallet to receive your tokens. You can launch @@ -298,7 +300,9 @@ export default function Launchpad() { onBlur={() => setTouched((t) => ({ ...t, symbol: true }))} maxLength={SYMBOL_MAX} - className={`h-11 rounded-xl text-sm ${symbolError ? 'border-destructive' : ''}`} + className={`h-11 rounded-xl text-sm ${ + symbolError ? 'border-destructive' : '' + }`} aria-invalid={!!symbolError} aria-describedby={symbolError ? 'launchpad-symbol-error' @@ -329,14 +333,16 @@ export default function Launchpad() { if (error) clearError(); }} onBlur={() => setTouched((t) => ({ ...t, decimals: true }))} - className={`h-11 rounded-xl text-sm ${decimalsError ? 'border-destructive' : ''}`} + className={`h-11 rounded-xl text-sm ${ + decimalsError ? 'border-destructive' : '' + }`} aria-invalid={!!decimalsError} /> {decimalsError && (

{decimalsError}

)}

- Between {DECIMALS_MIN} and {DECIMALS_MAX}. Standard is often 4–6. + Between {DECIMALS_MIN} and {DECIMALS_MAX}.

@@ -355,15 +361,16 @@ export default function Launchpad() { if (error) clearError(); }} onBlur={() => setTouched((t) => ({ ...t, supply: true }))} - className={`h-11 rounded-xl text-sm ${supplyError ? 'border-destructive' : ''}`} + className={`h-11 rounded-xl text-sm ${ + supplyError ? 'border-destructive' : '' + }`} aria-invalid={!!supplyError} /> {supplyError && (

{supplyError}

)}

- Whole tokens to mint (decimals apply on-chain). Max{' '} - {MAX_SUPPLY_DISPLAY} raw units. + Whole number of tokens to mint (without decimals)

@@ -409,18 +416,14 @@ export default function Launchpad() { role='note' > -

- Supply & Miden math.{' '} - This launchpad caps initial mint at{' '} +

+ Token supply{' '} + is capped at{' '} {MAX_SUPPLY_DISPLAY} {' '} - raw units (before decimals). Because of current Miden field arithmetic - limits, tokens whose total supply (especially with many decimals) - grows beyond what the protocol can represent safely may show rounding - issues, failed operations, or other unexpected behaviour—even when below - this UI cap. Prefer conservative supplies and decimals until those limits - evolve. + raw units. Operations with large token amounts paired with high decimals + may fail due to current Miden limitations.

From d00ec93e0797a7b0a9cb21e6692c29b137c6e964 Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 19 Mar 2026 17:51:36 +0100 Subject: [PATCH 46/49] Fixed eslint issues --- src/components/SwapPairs.tsx | 5 - src/components/XykTable/XykPoolTable.tsx | 17 +- src/hooks/useRpcWorker.ts | 13 +- src/lib/config.ts | 2 +- src/pages/Swap.tsx | 207 ++++++++++++++++++----- src/pages/XykPoolDetail.tsx | 145 +++++++++------- src/workers/rpc.worker.ts | 62 +++++-- 7 files changed, 314 insertions(+), 137 deletions(-) diff --git a/src/components/SwapPairs.tsx b/src/components/SwapPairs.tsx index c72fd0b..80322ae 100644 --- a/src/components/SwapPairs.tsx +++ b/src/components/SwapPairs.tsx @@ -1,12 +1,7 @@ -import { useState } from 'react'; - export const SwapPairs = ( { swapPairs, disabled }: { swapPairs: () => void; disabled: boolean }, ) => { - const [rotation, setRotation] = useState(0); - const handleClick = () => { - setRotation((r) => r + 180); swapPairs(); }; diff --git a/src/components/XykTable/XykPoolTable.tsx b/src/components/XykTable/XykPoolTable.tsx index f884292..63eefc7 100644 --- a/src/components/XykTable/XykPoolTable.tsx +++ b/src/components/XykTable/XykPoolTable.tsx @@ -15,7 +15,7 @@ export interface XykPoolTableProps { const normalize = (s: string) => s.trim().toLowerCase(); const XykPoolTable = ({ search }: XykPoolTableProps) => { - const { xykPools, refetch } = useXykPools(); + const { xykPools } = useXykPools(); const [page, setPage] = useState(0); const filteredPools = useMemo(() => { @@ -24,13 +24,15 @@ const XykPoolTable = ({ search }: XykPoolTableProps) => { const qNo0x = q.startsWith('0x') ? q.slice(2) : q; return xykPools.filter((p) => { const bech = accountIdToBech32(p.xykPoolId).toLowerCase(); - const hex = ((p.xykPoolId as unknown as { toHex?: () => string }).toHex?.() ?? '').toLowerCase(); + const hex = ((p.xykPoolId as unknown as { toHex?: () => string }).toHex?.() ?? '') + .toLowerCase(); const hexNo0x = hex.startsWith('0x') ? hex.slice(2) : hex; return bech.includes(q) || hex.includes(q) || hexNo0x.includes(qNo0x); }); }, [search, xykPools]); useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setPage(0); }, [search]); @@ -83,7 +85,10 @@ const XykPoolTable = ({ search }: XykPoolTableProps) => { ) : ( paginatedPools.map(pool => ( - + )) )} @@ -93,8 +98,10 @@ const XykPoolTable = ({ search }: XykPoolTableProps) => { {!isEmpty && totalPages > 1 && (
- {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, filteredPools.length)} of{' '} - {filteredPools.length} + {page * PAGE_SIZE + 1}–{Math.min( + (page + 1) * PAGE_SIZE, + filteredPools.length, + )} of {filteredPools.length}
)}
{/* Status badge */} -
+
{label} @@ -750,16 +859,20 @@ function InlineTxStatus({ - {formalBigIntFormat({ val: txInfo.sellAmount, expo: txInfo.sellToken.decimals })} - {' '}{txInfo.sellToken.symbol} + {formalBigIntFormat({ + val: txInfo.sellAmount, + expo: txInfo.sellToken.decimals, + })} {txInfo.sellToken.symbol} - {formalBigIntFormat({ val: txInfo.buyAmount, expo: txInfo.buyToken.decimals })} - {' '}{txInfo.buyToken.symbol} + {formalBigIntFormat({ + val: txInfo.buyAmount, + expo: txInfo.buyToken.decimals, + })} {txInfo.buyToken.symbol}
@@ -772,12 +885,14 @@ function InlineTxStatus({ onClick={copyNoteId} className='font-mono hover:bg-muted/50 rounded px-1 py-0.5 transition-colors cursor-pointer' > - {copied ? ( - - - Copied - - ) : truncateId(txInfo.noteId)} + {copied + ? ( + + + Copied + + ) + : truncateId(txInfo.noteId)}
(0); const [amountInStr, setAmountInStr] = useState(''); const [swapInputError, setSwapInputError] = useState(); const [swapProgressStep, setSwapProgressStep] = useState(null); - const [lastTrade, setLastTrade] = useState<{ - noteId: string; - txId: string | undefined; - amountIn: bigint; - amountOut: bigint; - sellSymbol: string; - buySymbol: string; - sellDecimals: number; - buyDecimals: number; - } | null>(null); + const [lastTrade, setLastTrade] = useState< + { + noteId: string; + txId: string | undefined; + amountIn: bigint; + amountOut: bigint; + sellSymbol: string; + buySymbol: string; + sellDecimals: number; + buyDecimals: number; + } | null + >(null); const hasPosition = lpBalance > BigInt(0); const poolSharePct = useMemo(() => { @@ -204,7 +206,15 @@ export default function XykPoolDetail() { buyDecimals: swapBuyToken.decimals, }); } - }, [poolData, swapSellToken, swapBuyToken, amountInBigint, expectedAmountOut, swap, waitForNoteConsumed]); + }, [ + poolData, + swapSellToken, + swapBuyToken, + amountInBigint, + expectedAmountOut, + swap, + waitForNoteConsumed, + ]); const openXykModal = useCallback( (mode: 'Deposit' | 'Withdraw') => { @@ -220,14 +230,6 @@ export default function XykPoolDetail() { [decodedPoolId, modalContext, refetchLpBalance], ); - const mockRecentTxs = useMemo(() => { - if (!poolData) return []; - return getMockRecentTransactions({ - seedKey: decodedPoolId ?? '', - baseSymbol: poolData.token0.symbol, - }); - }, [decodedPoolId, poolData]); - if (poolLoading && !poolData) { return ; } @@ -243,7 +245,7 @@ export default function XykPoolDetail() { } const pairLabel = `${poolData.token0.symbol} / ${poolData.token1.symbol}`; - const feeTier = feeTierForSymbol(poolData.token0.symbol); + const feeTier = feeTierForSymbol(); const priceDisplay = poolData.priceToken0InToken1 > 0 ? `1 ${poolData.token0.symbol} = ${ poolData.priceToken0InToken1.toFixed(6) @@ -254,21 +256,6 @@ export default function XykPoolDetail() { expo: 0, }); - const assetSymbol = (faucetIdBech32: string) => { - if (faucetIdBech32 === poolData.token0.faucetIdBech32) return poolData.token0.symbol; - if (faucetIdBech32 === poolData.token1.faucetIdBech32) return poolData.token1.symbol; - return null; - }; - const assetDecimals = (faucetIdBech32: string) => { - if (faucetIdBech32 === poolData.token0.faucetIdBech32) { - return poolData.token0.decimals; - } - if (faucetIdBech32 === poolData.token1.faucetIdBech32) { - return poolData.token1.decimals; - } - return 18; - }; - return ( - {poolData.token0.symbol} + + {poolData.token0.symbol} + - {prettyBigintFormat({ value: userToken0, expo: poolData.token0.decimals })} + {prettyBigintFormat({ + value: userToken0, + expo: poolData.token0.decimals, + })}
- {poolData.token1.symbol} + + {poolData.token1.symbol} + - {prettyBigintFormat({ value: userToken1, expo: poolData.token1.decimals })} + {prettyBigintFormat({ + value: userToken1, + expo: poolData.token1.decimals, + })}
@@ -535,30 +532,56 @@ export default function XykPoolDetail() { const noteId = isCurrent ? swapNoteId : lastTrade?.noteId; const txId = isCurrent ? swapTxId : lastTrade?.txId; const amountIn = isCurrent ? amountInBigint : lastTrade!.amountIn; - const amountOut = isCurrent ? expectedAmountOut : lastTrade!.amountOut; - const sellSym = isCurrent ? swapSellToken?.symbol ?? '—' : lastTrade!.sellSymbol; - const buySym = isCurrent ? swapBuyToken?.symbol ?? '—' : lastTrade!.buySymbol; - const sellDec = isCurrent ? (swapSellToken?.decimals ?? 18) : lastTrade!.sellDecimals; - const buyDec = isCurrent ? (swapBuyToken?.decimals ?? 18) : lastTrade!.buyDecimals; - const inStr = prettyBigintFormat({ value: amountIn, expo: sellDec }); - const outStr = prettyBigintFormat({ value: amountOut, expo: buyDec }); + const amountOut = isCurrent + ? expectedAmountOut + : lastTrade!.amountOut; + const sellSym = isCurrent + ? swapSellToken?.symbol ?? '—' + : lastTrade!.sellSymbol; + const buySym = isCurrent + ? swapBuyToken?.symbol ?? '—' + : lastTrade!.buySymbol; + const sellDec = isCurrent + ? (swapSellToken?.decimals ?? 18) + : lastTrade!.sellDecimals; + const buyDec = isCurrent + ? (swapBuyToken?.decimals ?? 18) + : lastTrade!.buyDecimals; + const inStr = prettyBigintFormat({ + value: amountIn, + expo: sellDec, + }); + const outStr = prettyBigintFormat({ + value: amountOut, + expo: buyDec, + }); return ( <>
- + {inStr} - {sellSym} + + {sellSym} +
- + {outStr} - {buySym} + + {buySym} +
diff --git a/src/workers/rpc.worker.ts b/src/workers/rpc.worker.ts index c1368b1..3c7626c 100644 --- a/src/workers/rpc.worker.ts +++ b/src/workers/rpc.worker.ts @@ -9,6 +9,7 @@ import { import type { RpcReady, SerializedWord, + SlotQuery, SlotResult, WorkerOutgoing, WorkerRequest, @@ -27,11 +28,21 @@ function getClient(rpcEndpoint: string): RpcClient { function wordToSerialized(w: Word): SerializedWord { const f = w.toFelts(); - return [f[0].asInt().toString(), f[1].asInt().toString(), f[2].asInt().toString(), f[3].asInt().toString()]; + return [ + f[0].asInt().toString(), + f[1].asInt().toString(), + f[2].asInt().toString(), + f[3].asInt().toString(), + ]; } function serializedToWord(s: SerializedWord): Word { - return Word.newFromFelts([new Felt(BigInt(s[0])), new Felt(BigInt(s[1])), new Felt(BigInt(s[2])), new Felt(BigInt(s[3]))]); + return Word.newFromFelts([ + new Felt(BigInt(s[0])), + new Felt(BigInt(s[1])), + new Felt(BigInt(s[2])), + new Felt(BigInt(s[3])), + ]); } // --- Caches --- @@ -40,7 +51,9 @@ const faucetCache = new Map(); const STORAGE_TTL_MS = 30_000; type AccountStorageObj = ReturnType< - NonNullable>['account']>>['storage'] + NonNullable< + ReturnType>['account']> + >['storage'] >; interface StorageCacheEntry { storage: AccountStorageObj | undefined; @@ -51,7 +64,10 @@ const storageCache = new Map(); // Dedup: if a fetch for the same account is already in-flight, reuse its promise const inflightStorage = new Map>(); -async function fetchAccountStorage(client: RpcClient, bech32: string): Promise { +async function fetchAccountStorage( + client: RpcClient, + bech32: string, +): Promise { const cached = storageCache.get(bech32); if (cached && Date.now() - cached.ts < STORAGE_TTL_MS) { return cached.storage; @@ -72,7 +88,10 @@ async function fetchAccountStorage(client: RpcClient, bech32: string): Promise { switch (q.kind) { case 'item': { @@ -81,17 +100,23 @@ function readSlots(storage: AccountStorageObj | undefined, queries: WorkerReques } case 'mapItem': { const value = storage?.getMapItem(q.slotName, serializedToWord(q.key)); - return { kind: 'mapItem' as const, value: value ? wordToSerialized(value) : null }; + return { + kind: 'mapItem' as const, + value: value ? wordToSerialized(value) : null, + }; } case 'mapEntries': { const entries = storage?.getMapEntries(q.slotName) ?? []; - return { kind: 'mapEntries' as const, entries: entries.map(e => ({ key: e.key, value: e.value })) }; + return { + kind: 'mapEntries' as const, + entries: entries.map(e => ({ key: e.key, value: e.value })), + }; } } }); } -async function handleMessage(msg: WorkerRequest): Promise { +async function handleMessage(msg: WorkerRequest) { const client = getClient(msg.rpcEndpoint); switch (msg.type) { @@ -105,7 +130,9 @@ async function handleMessage(msg: WorkerRequest): Promise { if (cached) { return { type: 'getFaucetInfo', id: msg.id, result: cached }; } - const fetched = await client.getAccountDetails(Address.fromBech32(msg.accountBech32).accountId()); + const fetched = await client.getAccountDetails( + Address.fromBech32(msg.accountBech32).accountId(), + ); const account = fetched.account(); if (!account) { return { type: 'getFaucetInfo', id: msg.id, result: null }; @@ -123,18 +150,23 @@ async function handleMessage(msg: WorkerRequest): Promise { return { type: 'invalidateCache', id: msg.id }; } default: - return { type: 'error', id: msg.id, message: `Unknown request type: ${(msg as { type: string }).type}` }; + return { + type: 'error', + id: -199999, + message: `Unknown request type: ${(msg as { type: string }).type}`, + } as WorkerOutgoing; } } self.onmessage = (e: MessageEvent) => { handleMessage(e.data).then( (response) => self.postMessage(response), - (err) => self.postMessage({ - type: 'error', - id: e.data.id, - message: err instanceof Error ? err.message : String(err), - }), + (err) => + self.postMessage({ + type: 'error', + id: e.data.id, + message: err instanceof Error ? err.message : String(err), + }), ); }; From 664f81b26228ecaa649580c2cf1fc3a21dcebdf2 Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 19 Mar 2026 18:00:25 +0100 Subject: [PATCH 47/49] Made status banner a bit smaller --- src/components/Header.tsx | 2 +- src/components/StatusBanner.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 0f69e24..03447a0 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -25,7 +25,6 @@ export function Header() { return ( <> -
{/* Desktop */}
@@ -121,6 +120,7 @@ export function Header() { )}
+ ); } diff --git a/src/components/StatusBanner.tsx b/src/components/StatusBanner.tsx index 0bc409e..c26be5d 100644 --- a/src/components/StatusBanner.tsx +++ b/src/components/StatusBanner.tsx @@ -82,7 +82,7 @@ export function StatusBanner() { return (
-
+

{banner.text} From 62e873213f8358f9a800864931e253fc695d2554 Mon Sep 17 00:00:00 2001 From: mico Date: Thu, 19 Mar 2026 19:47:11 +0100 Subject: [PATCH 48/49] Remove hardcoding --- src/components/IlRiskCard.tsx | 4 +- src/components/XykPoolModal.tsx | 156 ++++++++++-------- src/components/xyk-wizard/XykWizard.tsx | 15 +- .../xyk-wizard/steps/XykWizardStep1.tsx | 5 +- src/hooks/useTokensWithBalance.ts | 1 + src/hooks/useXykPools.tsx | 2 + src/lib/DeployXykPool.ts | 5 +- src/pages/Launchpad.tsx | 2 +- src/pages/XykPoolDetail.tsx | 18 +- 9 files changed, 121 insertions(+), 87 deletions(-) diff --git a/src/components/IlRiskCard.tsx b/src/components/IlRiskCard.tsx index 12c1f1b..33ed657 100644 --- a/src/components/IlRiskCard.tsx +++ b/src/components/IlRiskCard.tsx @@ -12,8 +12,8 @@ export function IlRiskCard() {

- This pool's tokens may have price correlation. Impermanent loss - is possible when prices move. Consider concentrated ranges carefully. + This pool's tokens may have price correlation. Impermanent loss/gain is + possible when prices move.

diff --git a/src/components/XykPoolModal.tsx b/src/components/XykPoolModal.tsx index 8d84730..3530f99 100644 --- a/src/components/XykPoolModal.tsx +++ b/src/components/XykPoolModal.tsx @@ -3,6 +3,7 @@ import { ProgressBar } from '@/components/ProgressBar'; import Slippage from '@/components/Slippage'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { getMidenscanNoteUrl, getMidenscanTxUrl } from '@/hooks/useLaunchpad'; import { useTokens } from '@/hooks/useTokens'; import { useWaitForNoteConsumed } from '@/hooks/useWaitForNoteConsumed'; import { useXykDeposit } from '@/hooks/useXykDeposit'; @@ -10,7 +11,6 @@ import { useXykLpBalance } from '@/hooks/useXykLpBalance'; import { useXykPool } from '@/hooks/useXykPool'; import type { XykTokenInfo } from '@/hooks/useXykPool'; import { useXykWithdraw } from '@/hooks/useXykWithdraw'; -import { getMidenscanNoteUrl, getMidenscanTxUrl } from '@/hooks/useLaunchpad'; import { DEFAULT_SLIPPAGE } from '@/lib/config'; import { formatTokenAmount, formatTokenAmountForInput } from '@/lib/format'; import { computeExpectedLp, computeExpectedWithdraw } from '@/lib/xykMath'; @@ -113,18 +113,20 @@ export function XykPoolModal({ const [depositPct, setDepositPct] = useState(100); const [withdrawPct, setWithdrawPct] = useState(100); const [lpProgressStep, setLpProgressStep] = useState(null); - const [lastLpAction, setLastLpAction] = useState<{ - type: 'Deposit' | 'Withdraw'; - noteId: string; - txId: string | undefined; - amount0: bigint; - amount1: bigint; - token0Symbol: string; - token1Symbol: string; - token0Decimals: number; - token1Decimals: number; - lpAmount?: bigint; - } | null>(null); + const [lastLpAction, setLastLpAction] = useState< + { + type: 'Deposit' | 'Withdraw'; + noteId: string; + txId: string | undefined; + amount0: bigint; + amount1: bigint; + token0Symbol: string; + token1Symbol: string; + token0Decimals: number; + token1Decimals: number; + lpAmount?: bigint; + } | null + >(null); const waitForNoteConsumed = useWaitForNoteConsumed({ timeoutMs: 60_000 }); @@ -807,9 +809,9 @@ export function XykPoolModal({ Impermanent Loss Notice

- Withdrawing now realizes any impermanent loss. Your position may have - experienced IL since deposit. If you deposited at a different price ratio, - you may receive fewer tokens than expected. + Withdrawing now realizes any impermanent loss or gain. Your position may + have experienced IL since deposit. If you deposited at a different price + ratio, you may receive fewer tokens than expected.

@@ -845,64 +847,72 @@ export function XykPoolModal({
{lastLpAction.type === 'Deposit' ? ( - <> -
- - - {formatTokenAmount({ - value: lastLpAction.amount0, - expo: lastLpAction.token0Decimals, - })} - - {lastLpAction.token0Symbol} -
- -
- - - {formatTokenAmount({ - value: lastLpAction.amount1, - expo: lastLpAction.token1Decimals, - })} - - {lastLpAction.token1Symbol} -
- - ) + <> +
+ + + {formatTokenAmount({ + value: lastLpAction.amount0, + expo: lastLpAction.token0Decimals, + })} + + + {lastLpAction.token0Symbol} + +
+ +
+ + + {formatTokenAmount({ + value: lastLpAction.amount1, + expo: lastLpAction.token1Decimals, + })} + + + {lastLpAction.token1Symbol} + +
+ + ) : ( - <> -
- - {lastLpAction.lpAmount != null - ? formatTokenAmount({ value: lastLpAction.lpAmount, expo: 0 }) - : '—'} - - LP -
- -
- - - {formatTokenAmount({ - value: lastLpAction.amount0, - expo: lastLpAction.token0Decimals, - })} - - {lastLpAction.token0Symbol} -
- + -
- - - {formatTokenAmount({ - value: lastLpAction.amount1, - expo: lastLpAction.token1Decimals, - })} - - {lastLpAction.token1Symbol} -
- - )} + <> +
+ + {lastLpAction.lpAmount != null + ? formatTokenAmount({ value: lastLpAction.lpAmount, expo: 0 }) + : '—'} + + LP +
+ +
+ + + {formatTokenAmount({ + value: lastLpAction.amount0, + expo: lastLpAction.token0Decimals, + })} + + + {lastLpAction.token0Symbol} + +
+ + +
+ + + {formatTokenAmount({ + value: lastLpAction.amount1, + expo: lastLpAction.token1Decimals, + })} + + + {lastLpAction.token1Symbol} + +
+ + )}
{ pool_acc: newPoolId, requestTransaction, client, + sender: accountId, }); + onProgress?.(3); + await client.syncState(); return newPoolId; } catch (e) { @@ -383,7 +387,16 @@ const XykWizard = () => { pairError={pairError} /> ); - }, [step, form, tokensWithBalance, restart, lastDeployedPoolIdBech32, hfAmmBech32s, registeredPairs, pairError]); + }, [ + step, + form, + tokensWithBalance, + restart, + lastDeployedPoolIdBech32, + hfAmmBech32s, + registeredPairs, + pairError, + ]); if (!connected) { return ( diff --git a/src/components/xyk-wizard/steps/XykWizardStep1.tsx b/src/components/xyk-wizard/steps/XykWizardStep1.tsx index 89aee07..44c53d9 100644 --- a/src/components/xyk-wizard/steps/XykWizardStep1.tsx +++ b/src/components/xyk-wizard/steps/XykWizardStep1.tsx @@ -173,9 +173,8 @@ const XykStep1 = (

- Base and quote (token0 / token1) are determined by the token account ID hex - value: the token with the lower{' '} - hex is base (token0), the other is quote (token1). + Base and quote order (token0 / token1) is based on the tokens' faucet account + IDs.

diff --git a/src/hooks/useTokensWithBalance.ts b/src/hooks/useTokensWithBalance.ts index 8a2a37f..a4e9932 100644 --- a/src/hooks/useTokensWithBalance.ts +++ b/src/hooks/useTokensWithBalance.ts @@ -12,6 +12,7 @@ const useTokensWithBalance = () => { const refresh = useCallback(async () => { setLoading(true); const tokens = await getAvailableTokens(); + console.log(tokens); setTokensWithBalance(tokens); setLoading(false); }, [getAvailableTokens]); diff --git a/src/hooks/useXykPools.tsx b/src/hooks/useXykPools.tsx index 23006d9..599f090 100644 --- a/src/hooks/useXykPools.tsx +++ b/src/hooks/useXykPools.tsx @@ -23,6 +23,8 @@ export const useXykPools = () => { { kind: 'mapEntries', slotName: 'zoro::registry::assets_to_pool_mapping' }, ]); + console.log(results); + const entries = (results[0] as SlotMapEntriesResult).entries; const pools: XykPool[] = []; diff --git a/src/lib/DeployXykPool.ts b/src/lib/DeployXykPool.ts index 438d7d9..ebd981b 100644 --- a/src/lib/DeployXykPool.ts +++ b/src/lib/DeployXykPool.ts @@ -251,9 +251,11 @@ export const registerPool = async ({ token0, token1, pool_acc, + sender, requestTransaction, }: DeployNewPoolParams & { pool_acc: AccountId; + sender: AccountId; requestTransaction: (tx: TransactionRequest) => void; }) => { if (!REGISTRY_ACCOUNT) return; @@ -262,9 +264,6 @@ export const registerPool = async ({ await client.importAccountById(pool_acc); await client.syncState(); - const sender = AccountId.fromBech32( - 'mtst1arw578zk4gfdzyzuzvj2xdlhds22nfkc_qruqqypuyph', - ); const script = compile_xyk_register_note_script(client); const noteTag = NoteTag.withAccountTarget(REGISTRY_ACCOUNT); const attachment = NoteAttachment.newNetworkAccountTarget( diff --git a/src/pages/Launchpad.tsx b/src/pages/Launchpad.tsx index 01e2f34..818d815 100644 --- a/src/pages/Launchpad.tsx +++ b/src/pages/Launchpad.tsx @@ -423,7 +423,7 @@ export default function Launchpad() { {MAX_SUPPLY_DISPLAY} {' '} raw units. Operations with large token amounts paired with high decimals - may fail due to current Miden limitations. + may fail due to current network limitations.

diff --git a/src/pages/XykPoolDetail.tsx b/src/pages/XykPoolDetail.tsx index da3d6ff..5f74e15 100644 --- a/src/pages/XykPoolDetail.tsx +++ b/src/pages/XykPoolDetail.tsx @@ -44,7 +44,12 @@ export default function XykPoolDetail() { const { poolId } = useParams<{ poolId: string }>(); const decodedPoolId = poolId ? decodeURIComponent(poolId) : undefined; const modalContext = useContext(ModalContext); - const { data: poolData, isLoading: poolLoading, error: poolError } = useXykPool( + const { + data: poolData, + isLoading: poolLoading, + error: poolError, + refetch: refetchPool, + } = useXykPool( decodedPoolId, ); const { lpBalance, refetch: refetchLpBalance } = useXykLpBalance(decodedPoolId); @@ -205,8 +210,10 @@ export default function XykPoolDetail() { sellDecimals: swapSellToken.decimals, buyDecimals: swapBuyToken.decimals, }); + refetchPool(); } }, [ + refetchPool, poolData, swapSellToken, swapBuyToken, @@ -223,11 +230,14 @@ export default function XykPoolDetail() { refetchLpBalance()} + onSuccess={() => { + refetchLpBalance(); + refetchPool(); + }} />, ); }, - [decodedPoolId, modalContext, refetchLpBalance], + [decodedPoolId, modalContext, refetchLpBalance, refetchPool], ); if (poolLoading && !poolData) { @@ -494,7 +504,7 @@ export default function XykPoolDetail() {

{swapInputError ?? swapError}

)}