diff --git a/.turbo/cache/6a91baf0e64ff848-meta.json b/.turbo/cache/6a91baf0e64ff848-meta.json new file mode 100644 index 000000000..6e9134e2a --- /dev/null +++ b/.turbo/cache/6a91baf0e64ff848-meta.json @@ -0,0 +1 @@ +{"hash":"6a91baf0e64ff848","duration":2858} \ No newline at end of file diff --git a/.turbo/cache/6a91baf0e64ff848.tar.zst b/.turbo/cache/6a91baf0e64ff848.tar.zst new file mode 100644 index 000000000..0f6b29376 Binary files /dev/null and b/.turbo/cache/6a91baf0e64ff848.tar.zst differ diff --git a/.turbo/cache/819d866a1222082a-meta.json b/.turbo/cache/819d866a1222082a-meta.json new file mode 100644 index 000000000..1d360b3ea --- /dev/null +++ b/.turbo/cache/819d866a1222082a-meta.json @@ -0,0 +1 @@ +{"hash":"819d866a1222082a","duration":3036} \ No newline at end of file diff --git a/.turbo/cache/819d866a1222082a.tar.zst b/.turbo/cache/819d866a1222082a.tar.zst new file mode 100644 index 000000000..a8f74aa17 Binary files /dev/null and b/.turbo/cache/819d866a1222082a.tar.zst differ diff --git a/.turbo/cache/f0be77d340149c0e-meta.json b/.turbo/cache/f0be77d340149c0e-meta.json new file mode 100644 index 000000000..661251489 --- /dev/null +++ b/.turbo/cache/f0be77d340149c0e-meta.json @@ -0,0 +1 @@ +{"hash":"f0be77d340149c0e","duration":3880} \ No newline at end of file diff --git a/.turbo/cache/f0be77d340149c0e.tar.zst b/.turbo/cache/f0be77d340149c0e.tar.zst new file mode 100644 index 000000000..170850eb1 Binary files /dev/null and b/.turbo/cache/f0be77d340149c0e.tar.zst differ diff --git a/.turbo/cache/f8b2adaa0614c467-meta.json b/.turbo/cache/f8b2adaa0614c467-meta.json new file mode 100644 index 000000000..28821448e --- /dev/null +++ b/.turbo/cache/f8b2adaa0614c467-meta.json @@ -0,0 +1 @@ +{"hash":"f8b2adaa0614c467","duration":5375} \ No newline at end of file diff --git a/.turbo/cache/f8b2adaa0614c467.tar.zst b/.turbo/cache/f8b2adaa0614c467.tar.zst new file mode 100644 index 000000000..1bb9ff93c Binary files /dev/null and b/.turbo/cache/f8b2adaa0614c467.tar.zst differ diff --git a/.turbo/cookies/10.cookie b/.turbo/cookies/10.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/11.cookie b/.turbo/cookies/11.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/12.cookie b/.turbo/cookies/12.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/2.cookie b/.turbo/cookies/2.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/3.cookie b/.turbo/cookies/3.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/4.cookie b/.turbo/cookies/4.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/5.cookie b/.turbo/cookies/5.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/6.cookie b/.turbo/cookies/6.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/7.cookie b/.turbo/cookies/7.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/8.cookie b/.turbo/cookies/8.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/cookies/9.cookie b/.turbo/cookies/9.cookie new file mode 100644 index 000000000..e69de29bb diff --git a/.turbo/daemon/c9749efa7ac2748e-turbo.log.2025-11-19 b/.turbo/daemon/c9749efa7ac2748e-turbo.log.2025-11-19 new file mode 100644 index 000000000..e69de29bb diff --git a/apps/docus/docusaurus.config.ts b/apps/docus/docusaurus.config.ts index 74f06d1ca..00ad024c0 100644 --- a/apps/docus/docusaurus.config.ts +++ b/apps/docus/docusaurus.config.ts @@ -76,7 +76,7 @@ const config: Config = { // Replace with your project's social card image: 'img/docusaurus-social-card.jpg', navbar: { - title: 'TrusSwap Docs', + title: 'TrustSwap Docs', items: [ { type: 'doc', docId: 'litepaper/introduction', position: 'left', label: 'Litepaper' }, diff --git a/apps/web/src/components/Layout.tsx b/apps/web/src/components/Layout.tsx index b33dcf075..eff8cd71a 100644 --- a/apps/web/src/components/Layout.tsx +++ b/apps/web/src/components/Layout.tsx @@ -2,6 +2,7 @@ import { NavLink, Outlet, useLocation } from "react-router-dom"; import { useEffect, useRef, useState } from "react"; import styles from "../styles/Layout.module.css"; import { ConnectButton } from "./ConnectButton"; +import { NetworkSelect } from "./NetworkSelect"; import logo from "../assets/logo.png"; export default function Layout() { @@ -113,6 +114,9 @@ export default function Layout() {
+
+ +
diff --git a/apps/web/src/components/NetworkSelect.tsx b/apps/web/src/components/NetworkSelect.tsx new file mode 100644 index 000000000..de3d9ef60 --- /dev/null +++ b/apps/web/src/components/NetworkSelect.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { useChainId, useSwitchChain } from "wagmi"; +import { CHAINS } from "../lib/wagmi"; +import styles from "../styles/Layout.module.css"; + +export function NetworkSelect() { + const chainId = useChainId(); + const { switchChainAsync } = useSwitchChain(); + + const handleChange = async (event: React.ChangeEvent) => { + const targetId = Number(event.target.value); + const targetChain = CHAINS.find((c) => c.id === targetId); + if (!targetChain) return; + + try { + // Wait for chain switch to complete + await switchChainAsync({ chainId: targetChain.id }); + + // Hard reload to reset all React state / caches + window.location.reload(); + } catch (err) { + console.error("Failed to switch chain", err); + } + }; + + return ( + + ); +} diff --git a/apps/web/src/config/sdk.ts b/apps/web/src/config/sdk.ts index ac96fd641..28bd93e9c 100644 --- a/apps/web/src/config/sdk.ts +++ b/apps/web/src/config/sdk.ts @@ -1,16 +1,19 @@ // apps/web/src/config/sdk.ts import type { Address } from "viem"; -// Source de vérité: SDK (pas le wallet) -import { INTUITION, addresses as SDK_ADDRESSES } from "@trustswap/sdk"; +import { + INTUITION, + INTUITION_MAINNET, + INTUITION_TESTNET, + getAddresses, +} from "@trustswap/sdk"; // --- Types "côté app" (normalisés) --- export type ProtocolAddresses = { ROUTER02: Address; FACTORY: Address; WNATIVE: Address; - // (optionnel) expose aussi ce dont tu as besoin dans l'app: TSWP?: Address; - SRF?: Address; // StakingRewardsFactory si tu l'as + SRF?: Address; }; export type ProtocolSymbols = { @@ -19,51 +22,66 @@ export type ProtocolSymbols = { }; // --- Choix du chainId app --- -// Priorité à l'ENV si tu veux override, sinon SDK -const APP_CHAIN_ID = Number(import.meta.env.VITE_CHAIN_ID || INTUITION.id); +// ENV > sinon on prend le chain du SDK (INTUITION = "courant") +const APP_CHAIN_ID = + Number(import.meta.env.VITE_CHAIN_ID || INTUITION.id); // --- Adaptateur: mappe les noms du SDK vers tes noms normalisés --- function normalizeSdkAddresses(a: any): ProtocolAddresses { - // Adapte ces clés selon ce que ton SDK expose exactement return { ROUTER02: (a.UniswapV2Router02 ?? a.ROUTER02) as Address, - FACTORY: (a.UniswapV2Factory ?? a.FACTORY) as Address, - WNATIVE: (a.WTTRUST ?? a.WNATIVE ?? a.WETH9) as Address, - TSWP: a.TSWP as Address, - SRF: (a.SRF ?? a.StakingRewardsFactory ?? a.StakingRewardsFactoryV2) as Address, + FACTORY: (a.UniswapV2Factory ?? a.FACTORY) as Address, + // wrapped: WTRUST sur mainnet, WTTRUST sur testnet, WNATIVE/WETH9 en fallback + WNATIVE: (a.WTRUST ?? a.WTTRUST ?? a.WNATIVE ?? a.WETH9) as Address, + TSWP: a.TSWP as Address, + SRF: (a.SRF ?? a.StakingRewardsFactory ?? a.StakingRewardsFactoryV2) as Address, }; } -// Si ton SDK n'est pas multi-chain, on normalise directement l'objet plat: -const FROM_SDK: ProtocolAddresses = normalizeSdkAddresses(SDK_ADDRESSES); - -// Fallbacks (utile si le SDK n'est pas dispo / dev offline) +// Fallbacks explicites si jamais getAddresses ne connaît pas encore la chain const FALLBACK_ADDRESSES: Record = { - 13579: { + // 🔹 Intuition Testnet + [INTUITION_TESTNET.id]: { ROUTER02: "0xAc1218b429E2BB26f5FFe635F04F7412ac40979c" as Address, FACTORY: "0xd103E057242881214793d5A1A7c2A5B84731c75c" as Address, WNATIVE: "0x51379Cc2C942EE2AE2fF0BD67a7b475F0be39Dcf" as Address, TSWP: "0x7da120065e104C085fAc6f800d257a6296549cF3" as Address, - // SRF: "0x819030e047cB49E9F68599433FeC5A7C32B41565" as Address, // si besoin + }, + + // 🔹 Intuition Mainnet + [INTUITION_MAINNET.id]: { + ROUTER02: "0x5123208Aa3C6A37615327a8c479a5e1654c0200E" as Address, + FACTORY: "0x83E9f4E539eb343F7F67d130a484c8a1b6555458" as Address, + WNATIVE: "0x81cFb09cb44f7184Ad934C09F82000701A4bF672" as Address, // WTRUST + // TSWP: "0x...." as Address, // quand tu le lances }, }; -// Symboles (depuis le SDK + override) +// Symboles (depuis le SDK + override par chain) const FALLBACK_SYMBOLS: Record = { - 13579: { - NATIVE_SYMBOL: INTUITION.nativeCurrency.symbol || "tTRUST", + [INTUITION_TESTNET.id]: { + NATIVE_SYMBOL: INTUITION_TESTNET.nativeCurrency.symbol || "tTRUST", WRAPPED_SYMBOL: "WTTRUST", }, + [INTUITION_MAINNET.id]: { + NATIVE_SYMBOL: INTUITION_MAINNET.nativeCurrency.symbol || "TRUST", + WRAPPED_SYMBOL: "WTRUST", + }, }; // --- API exportée par le module --- export function getProtocolConfig() { - let addrs: ProtocolAddresses | undefined = FROM_SDK; + let addrs: ProtocolAddresses | undefined; - if (!addrs?.ROUTER02 || !addrs?.FACTORY || !addrs?.WNATIVE) { + try { + const sdkAddrs = getAddresses(APP_CHAIN_ID); + addrs = normalizeSdkAddresses(sdkAddrs); + } catch { + // si le SDK ne connaît pas encore la chain -> fallback local addrs = FALLBACK_ADDRESSES[APP_CHAIN_ID]; } - if (!addrs) { + + if (!addrs?.ROUTER02 || !addrs?.FACTORY || !addrs?.WNATIVE) { throw new Error(`Aucune config protocole pour chainId=${APP_CHAIN_ID}`); } diff --git a/apps/web/src/features/pool/components/PoolRow.tsx b/apps/web/src/features/pool/components/PoolRow.tsx index 75052032a..853a65820 100644 --- a/apps/web/src/features/pool/components/PoolRow.tsx +++ b/apps/web/src/features/pool/components/PoolRow.tsx @@ -8,10 +8,10 @@ import { Volume1DCell } from "./cells/Volume1DCell"; import { PoolAprCell } from "./cells/PoolAprCell"; import { PoolActionsCell } from "./cells/PoolActionsCell"; import styles from "../tableau.module.css"; - -import { WNATIVE_ADDRESS } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; function asUIToken(t: T): T { + const { WNATIVE_ADDRESS } = useTokenModule(); const isWNative = t?.address?.toLowerCase() === WNATIVE_ADDRESS.toLowerCase(); if (!isWNative) return t; return { ...t, symbol: "tTRUST" } as T; @@ -49,7 +49,7 @@ export function PoolRow({ > - {/* Display the tokens UI (symbol tTRUST if WTTRUST) */} + {/* Display the tokens UI (symbol tTRUST if WTRUST) */} {/* Pass also the UI tokens if needed (if TvlCell displays the symbols somewhere) */} diff --git a/apps/web/src/features/pool/components/PoolsPage.tsx b/apps/web/src/features/pool/components/PoolsPage.tsx index d847befa8..c4ca308aa 100644 --- a/apps/web/src/features/pool/components/PoolsPage.tsx +++ b/apps/web/src/features/pool/components/PoolsPage.tsx @@ -8,19 +8,23 @@ import { PoolsTable } from "./PoolsTable"; import { PoolsFilters } from "./filters/PoolsFilters"; import { PoolsPagination } from "./filters/PoolsPagination"; import { LiquidityModal } from "./liquidity/LiquidityModal"; -import { toUIAddress } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; import styles from "../pools.module.css"; export default function PoolsPage() { + const [page, setPage] = useState(1); const [query, setQuery] = useState(""); + const [hasNextPage, setHasNextPage] = useState(false); + const [isOpen, setIsOpen] = useState(false); const [tokenA, setTokenA] = useState
(); const [tokenB, setTokenB] = useState
(); + const { toUIAddress } = useTokenModule(); const pc = usePublicClient({ chainId: 13579 }); @@ -88,8 +92,14 @@ export default function PoolsPage() { page={page} query={query} onOpenLiquidity={openWithPair} + onPageInfoChange={(info) => setHasNextPage(info.hasNextPage)} + /> + + - )} diff --git a/apps/web/src/features/pool/components/PoolsTable.tsx b/apps/web/src/features/pool/components/PoolsTable.tsx index 6b8139b69..edc1970a6 100644 --- a/apps/web/src/features/pool/components/PoolsTable.tsx +++ b/apps/web/src/features/pool/components/PoolsTable.tsx @@ -1,5 +1,5 @@ // apps/web/src/features/pools/components/PoolsTable.tsx -import { useMemo } from "react"; +import { useMemo, useEffect } from "react"; import type { Address } from "viem"; import { usePoolsData } from "../hooks/usePoolsData"; @@ -17,14 +17,24 @@ export function PoolsTable({ page, query, onOpenLiquidity, + onPageInfoChange, }: { page: number; query: string; onOpenLiquidity: (a: Address, b: Address) => void; + onPageInfoChange?: (info: { hasNextPage: boolean }) => void; }) { - const pageSize = 10; + const pageSize = 8; const { items, loading, error } = usePoolsData(pageSize, (page - 1) * pageSize); + useEffect(() => { + if (!onPageInfoChange) return; + if (loading || error) return; + + // Si on a une page "pleine", il y a potentiellement une page suivante + onPageInfoChange({ hasNextPage: items.length === pageSize }); + }, [items.length, loading, error, onPageInfoChange]); + const skeletonPool: PoolItem = { pair: "0x0000000000000000000000000000000000000000", token0: { symbol: "", address: "" as `0x${string}`, decimals: 18 }, @@ -72,7 +82,9 @@ export function PoolsTable({ ); } - if (!items.length) return
Aucune pool
; + if (!items.length) + return
No pools available
; + return ( void; }) { - if (totalPages <= 1) return null; + if (page === 1 && !hasNextPage) return null; return (
@@ -18,10 +19,12 @@ export function PoolsPagination({ Prev )} - Page {page} - {page < totalPages && ( - + {page} + {hasNextPage && ( + )}
); -} +} \ No newline at end of file diff --git a/apps/web/src/features/pool/components/liquidity/AddLiquidityDrawer.tsx b/apps/web/src/features/pool/components/liquidity/AddLiquidityDrawer.tsx index b47471bcf..9932df88d 100644 --- a/apps/web/src/features/pool/components/liquidity/AddLiquidityDrawer.tsx +++ b/apps/web/src/features/pool/components/liquidity/AddLiquidityDrawer.tsx @@ -3,13 +3,14 @@ import type { Address } from "viem"; import { parseUnits } from "viem"; import { useAccount, usePublicClient } from "wagmi"; import { useLiquidityActions } from "../../hooks/useLiquidityActions"; -import { toWrapped } from "../../../../lib/tokens"; import { getTokenIcon } from "../../../../lib/getTokenIcon"; import styles from "../../modal.module.css"; import TokenField from "../../../swap/components/TokenField"; import { quoteOutFromReserves } from "../../../../utils/quotes"; import { abi, addresses } from "@trustswap/sdk"; -import { isZeroAddress } from "../../../../lib/erc20Read"; +import { useErc20Read } from "../../../../lib/erc20Read"; +import { useTokenModule } from "../../../../hooks/useTokenModule"; + type PairData = { pair: Address; @@ -34,6 +35,8 @@ export function AddLiquidityDrawer({ }) { const { address: to } = useAccount(); const pc = usePublicClient(); + const { toWrapped } = useTokenModule(); + const { isZeroAddress } = useErc20Read(); const { addLiquidity } = useLiquidityActions(); const [tokenIn, setTokenIn] = useState
(tokenA); diff --git a/apps/web/src/features/pool/components/liquidity/RemoveLiquidityDrawer.tsx b/apps/web/src/features/pool/components/liquidity/RemoveLiquidityDrawer.tsx index 32a7d9be2..777e877ff 100644 --- a/apps/web/src/features/pool/components/liquidity/RemoveLiquidityDrawer.tsx +++ b/apps/web/src/features/pool/components/liquidity/RemoveLiquidityDrawer.tsx @@ -5,7 +5,8 @@ import { formatUnits, parseUnits } from "viem"; import styles from "../../modal.module.css"; import { clampDecimalsForInput, tidyOnBlur } from "../../../../utils/number"; import { useLiquidityActions } from "../../hooks/useLiquidityActions"; -import { toWrapped } from "../../../../lib/tokens"; +import { useTokenModule } from "../../../../hooks/useTokenModule"; + import { getTokenIcon } from "../../../../lib/getTokenIcon"; import { useLpPosition } from "../../hooks/useLpPosition"; import { fmtUnits, formatAmountStr } from "../../utils"; @@ -29,6 +30,8 @@ export function RemoveLiquidityDrawer({ const [lpAmount, setLpAmount] = useState(""); const [lpRawOverride, setLpRawOverride] = useState(null); + const { toWrapped } = useTokenModule(); + // Position LP réelle const { diff --git a/apps/web/src/features/pool/hooks/useGlobalStats.ts b/apps/web/src/features/pool/hooks/useGlobalStats.ts index 46e0db30d..3fa53ef4d 100644 --- a/apps/web/src/features/pool/hooks/useGlobalStats.ts +++ b/apps/web/src/features/pool/hooks/useGlobalStats.ts @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; import type { Abi, Address } from "viem"; import { parseAbiItem } from "viem"; -import { usePublicClient } from "wagmi"; -import { addresses } from "@trustswap/sdk"; +import { usePublicClient, useChainId } from "wagmi"; +import { getAddresses } from "@trustswap/sdk"; import * as SDKAbi from "@trustswap/sdk/abi"; -import { WNATIVE_ADDRESS } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; function toAbi(x: unknown): Abi { return (Array.isArray(x) ? x : (x as any)?.abi) as Abi; @@ -22,14 +22,18 @@ type PairMeta = { t1: Address; r0: bigint; r1: bigint; - wIs0?: boolean; // true si t0==WNATIVE, false si t1==WNATIVE, undefined sinon + wIs0?: boolean; }; export function useGlobalStats() { - const pc = usePublicClient(); + const wagmiChainId = useChainId(); + const fallbackChainId = wagmiChainId; + const pc = usePublicClient({ chainId: fallbackChainId }); + const [data, setData] = useState<{ tvlWT: bigint; vol24hWT: bigint; tx24h: number } | null>(null); const [loading, setLoading] = useState(true); const [error, setErr] = useState(null); + const { WNATIVE_ADDRESS } = useTokenModule(); useEffect(() => { (async () => { @@ -42,7 +46,9 @@ export function useGlobalStats() { return; } - const factory = addresses.UniswapV2Factory as Address; + const activeChainId = pc.chain?.id ?? fallbackChainId; + const { UniswapV2Factory } = getAddresses(Number(activeChainId)); + const factory = UniswapV2Factory as Address; // 1) pairs const len = await pc.readContract({ @@ -80,6 +86,7 @@ export function useGlobalStats() { ])), }); + const w = WNATIVE_ADDRESS.toLowerCase(); const metas: PairMeta[] = []; const pairIndex: Record = {}; diff --git a/apps/web/src/features/pool/hooks/useLiquidityActions.ts b/apps/web/src/features/pool/hooks/useLiquidityActions.ts index 214609023..099cce011 100644 --- a/apps/web/src/features/pool/hooks/useLiquidityActions.ts +++ b/apps/web/src/features/pool/hooks/useLiquidityActions.ts @@ -3,8 +3,8 @@ import { useWalletClient, usePublicClient, useChainId } from "wagmi"; import type { Address, Abi } from "viem"; import { erc20Abi, maxUint256, parseGwei, zeroAddress } from "viem"; import { addresses } from "@trustswap/sdk"; -import { toWrapped, WNATIVE_ADDRESS } from "../../../lib/tokens"; import { useAlerts } from "../../../features/alerts/Alerts"; +import { useTokenModule } from "../../../hooks/useTokenModule"; // --- Réseau / addresses const ROUTER = addresses.UniswapV2Router02 as Address; @@ -158,6 +158,7 @@ export function useLiquidityActions() { const publicClient = usePublicClient(); const chainId = useChainId(); const alerts = useAlerts(); + const { toWrapped, WNATIVE_ADDRESS } = useTokenModule(); async function estimateOverrides(base: { address: Address; @@ -242,7 +243,7 @@ export function useLiquidityActions() { } } - // WTTRUST = vrai wrapped natif (avec alertes) + // WTRUST = vrai wrapped natif (avec alertes) async function wrapNative(amount: bigint) { try { const base = { diff --git a/apps/web/src/features/pool/hooks/useLpPosition.ts b/apps/web/src/features/pool/hooks/useLpPosition.ts index 700e41ab0..0757862fa 100644 --- a/apps/web/src/features/pool/hooks/useLpPosition.ts +++ b/apps/web/src/features/pool/hooks/useLpPosition.ts @@ -4,13 +4,14 @@ import type { Address } from "viem"; import { useAccount, usePublicClient } from "wagmi"; import { erc20Abi, formatUnits, zeroAddress } from "viem"; import { abi, addresses } from "@trustswap/sdk"; -import { - NATIVE_PLACEHOLDER, - WNATIVE_ADDRESS, - getOrFetchToken, -} from "../../../lib/tokens"; + +import { useTokenModule } from "../../../hooks/useTokenModule"; + + function toERC20ForRead(addr?: Address): Address | undefined { + const { NATIVE_PLACEHOLDER, WNATIVE_ADDRESS } = useTokenModule(); + if (!addr) return undefined; return addr.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase() ? (WNATIVE_ADDRESS as Address) @@ -49,6 +50,9 @@ export function useLpPosition(tokenA?: Address, tokenB?: Address): LpPosition { const pc = usePublicClient(); // ✅ pas de chainId forcé ici const { address: owner } = useAccount(); + const { getOrFetchToken } = useTokenModule(); + + // adresses “lecture” (toujours ERC-20) const readA = toERC20ForRead(tokenA); const readB = toERC20ForRead(tokenB); @@ -131,7 +135,7 @@ export function useLpPosition(tokenA?: Address, tokenB?: Address): LpPosition { // 5) Décimales pour format (safe on-chain). // Ici on prend celles des ERC-20 de la pair (readA/readB). - // WTTRUST a 18, et les ERC-20 importés seront lus on-chain via getOrFetchToken. + // WTRUST a 18, et les ERC-20 importés seront lus on-chain via getOrFetchToken. const [metaA, metaB] = await Promise.all([ getOrFetchToken(readA), getOrFetchToken(readB), diff --git a/apps/web/src/features/pool/hooks/usePairMetrics.ts b/apps/web/src/features/pool/hooks/usePairMetrics.ts index b9f97df13..210790297 100644 --- a/apps/web/src/features/pool/hooks/usePairMetrics.ts +++ b/apps/web/src/features/pool/hooks/usePairMetrics.ts @@ -3,7 +3,7 @@ import { useMemo } from "react"; import type { PoolItem } from "../types"; import { aprFromFees } from "../utils"; import { formatUnits } from "viem"; -import { WNATIVE_ADDRESS } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; function safeUnits(x: unknown, decimals: number): number { try { @@ -19,6 +19,7 @@ export function usePairMetrics( volMap: Record = {}, priceMap: Record = {} ) { + const { WNATIVE_ADDRESS } = useTokenModule(); const w = WNATIVE_ADDRESS.toLowerCase(); return useMemo(() => { diff --git a/apps/web/src/features/pool/hooks/usePairsVolume1D.ts b/apps/web/src/features/pool/hooks/usePairsVolume1D.ts index 378bf42b1..844ebf473 100644 --- a/apps/web/src/features/pool/hooks/usePairsVolume1D.ts +++ b/apps/web/src/features/pool/hooks/usePairsVolume1D.ts @@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { parseAbiItem, formatUnits } from "viem"; import { usePublicClient } from "wagmi"; -import { WNATIVE_ADDRESS } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + import type { PoolItem } from "../types"; // Adjust the path if PoolItem is defined elsewhere const swapEvent = parseAbiItem( @@ -14,6 +15,7 @@ export function usePairsVolume1D(items: PoolItem[]) { const [volMap, setVolMap] = useState>({}); const [priceMap, setPriceMap] = useState>({}); const runIdRef = useRef(0); + const { WNATIVE_ADDRESS } = useTokenModule(); // 🔑 clé stable quand les paires changent const pairsKey = useMemo(() => { diff --git a/apps/web/src/features/pool/hooks/usePoolsData.ts b/apps/web/src/features/pool/hooks/usePoolsData.ts index f096403bc..901b1863b 100644 --- a/apps/web/src/features/pool/hooks/usePoolsData.ts +++ b/apps/web/src/features/pool/hooks/usePoolsData.ts @@ -1,9 +1,10 @@ // apps/web/src/features/pools/hooks/usePoolsData.ts import { useEffect, useRef, useState } from "react"; import { type Address, type Abi } from "viem"; -import { usePublicClient } from "wagmi"; -import { abi, addresses } from "@trustswap/sdk"; -import { getOrFetchToken } from "../../../lib/tokens"; +import { usePublicClient, useChainId } from "wagmi"; +import { abi, getAddresses } from "@trustswap/sdk"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + import type { PoolItem } from "../types"; const chunk = (a: T[], n = 300) => @@ -15,28 +16,49 @@ const PAIR_ABI = toAbi(abi.UniswapV2Pair); const dbg = (...args: any[]) => console.log("[usePoolsData]", ...args); export function usePoolsData(limit = 50, offset = 0) { - const pc = usePublicClient({ chainId: 13579 }); + const wagmiChainId = useChainId(); + const fallbackChainId = wagmiChainId; + + // Let wagmi give us the right client for the current chain + const pc = usePublicClient({ chainId: fallbackChainId }); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [items, setItems] = useState([]); const runIdRef = useRef(0); + const { getOrFetchToken } = useTokenModule(); useEffect(() => { - if (!pc) { dbg("no public client"); return; } + if (!pc) { + dbg("no public client"); + return; + } + + const activeChainId = pc.chain?.id ?? fallbackChainId; + const { UniswapV2Factory, StakingRewardsFactory } = getAddresses(Number(activeChainId)); - dbg("start", { limit, offset, chainId: pc?.chain?.id, hasMulticall3: !!pc.chain?.contracts?.multicall3 }); + dbg("start", { + limit, + offset, + chainId: activeChainId, + hasMulticall3: !!pc.chain?.contracts?.multicall3, + }); const runId = ++runIdRef.current; + (async () => { try { - setLoading(true); setError(null); + setLoading(true); + setError(null); - const factory = addresses.UniswapV2Factory as Address; + const factory = UniswapV2Factory as Address; const canMulticall = !!pc.chain?.contracts?.multicall3; dbg("factory", factory); const total = await pc.readContract({ - address: factory, abi: FACTORY_ABI, functionName: "allPairsLength", + address: factory, + abi: FACTORY_ABI, + functionName: "allPairsLength", }) as bigint; dbg("total pairs", total?.toString()); @@ -45,7 +67,6 @@ export function usePoolsData(limit = 50, offset = 0) { const idxs = Array.from({ length: end - start }, (_, i) => BigInt(start + i)); dbg("range", { start, end, count: idxs.length }); - // 1) Pairs const pairs: Address[] = []; for (const ids of chunk(idxs, 900)) { dbg("fetch pairs chunk", { size: ids.length, mode: canMulticall ? "multicall" : "loop" }); @@ -53,18 +74,29 @@ export function usePoolsData(limit = 50, offset = 0) { ? await pc.multicall({ allowFailure: false, contracts: ids.map((i) => ({ - address: factory, abi: FACTORY_ABI, functionName: "allPairs", args: [i], + address: factory, + abi: FACTORY_ABI, + functionName: "allPairs", + args: [i], })), }) : await Promise.all(ids.map((i) => - pc.readContract({ address: factory, abi: FACTORY_ABI, functionName: "allPairs", args: [i] }) + pc.readContract({ + address: factory, + abi: FACTORY_ABI, + functionName: "allPairs", + args: [i], + }), )); pairs.push(...(res as Address[])); dbg("pairs so far", pairs.length); } - if (!pairs.length) { if (runId === runIdRef.current) setItems([]); return; } - // 2) token0 / token1 / reserves + if (!pairs.length) { + if (runId === runIdRef.current) setItems([]); + return; + } + type Meta = { pair: Address; t0: Address; t1: Address; r0: bigint; r1: bigint }; const metas: Meta[] = []; @@ -102,22 +134,24 @@ export function usePoolsData(limit = 50, offset = 0) { dbg("metas so far", metas.length); } - // 3) Enrichissement metadata tokens dbg("enrich tokens", { count: metas.length }); - const rows: PoolItem[] = await Promise.all(metas.map(async (m) => { - const [t0Info, t1Info] = await Promise.all([ - getOrFetchToken(m.t0), getOrFetchToken(m.t1), - ]); - return { - pair: m.pair, - token0: t0Info, - token1: t1Info, - reserve0: m.r0, - reserve1: m.r1, - srf: addresses.StakingRewardsFactory as Address, - staking: null, - } satisfies PoolItem; - })); + const rows: PoolItem[] = await Promise.all( + metas.map(async (m) => { + const [t0Info, t1Info] = await Promise.all([ + getOrFetchToken(m.t0), + getOrFetchToken(m.t1), + ]); + return { + pair: m.pair, + token0: t0Info, + token1: t1Info, + reserve0: m.r0, + reserve1: m.r1, + srf: StakingRewardsFactory as Address, + staking: null, + } satisfies PoolItem; + }), + ); dbg("rows ready", rows.length); if (runId === runIdRef.current) setItems(rows); @@ -129,7 +163,7 @@ export function usePoolsData(limit = 50, offset = 0) { dbg("done"); } })(); - }, [pc?.chain?.id, limit, offset]); + }, [pc, fallbackChainId, limit, offset, getOrFetchToken]); return { loading, error, items }; } diff --git a/apps/web/src/features/pool/hooks/useStakingData.ts b/apps/web/src/features/pool/hooks/useStakingData.ts index 08d0ae606..631099d5a 100644 --- a/apps/web/src/features/pool/hooks/useStakingData.ts +++ b/apps/web/src/features/pool/hooks/useStakingData.ts @@ -5,13 +5,13 @@ import type { Address, Abi } from "viem"; import { erc20Abi, formatUnits, zeroAddress } from "viem"; import { addresses } from "@trustswap/sdk"; import type { PoolItem } from "../types"; -import { getOrFetchToken, WNATIVE_ADDRESS } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + import { FARMS } from "../../../lib/farms"; import { useLiveRegister } from "../../../live/LiveRefetchProvider"; const SEC_PER_YEAR = 31_536_000; const FEE_TO_LPS = 0.003; // 0.3% fees distributed to LPs - // --- ABIs --- const STAKING_ABI = [ { type: "function", name: "rewardRate", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] }, @@ -34,28 +34,30 @@ const PAIR_ABI = [ ] }, ] as const satisfies Abi; -type StakingSlice = { - staking: Address | null; - rewardToken?: Awaited>; - rewardRatePerSec?: bigint; - earned?: bigint; - stakedBalance?: bigint; // user staked LP - walletLpBalance?: bigint; // user wallet LP (outside farm) - periodFinish?: bigint; // timestamp (sec) - periodFinishDate?: Date; - - totalStakedLP?: bigint; // farm total staked LP (all users) - totalSupplyLP?: bigint; // LP token total supply - poolReserves?: { token0: Address; token1: Address; reserve0: bigint; reserve1: bigint }; - - poolAprPct?: number; // fees APR (trading fees -> LPs) - epochAprPct?: number; // farming APR (global) - epochAprUserPct?: number; // OPTIONAL: user-specific APR -}; export function useStakingData(pools: PoolItem[]) { const { address: user } = useAccount(); const client = usePublicClient(); + const {WNATIVE_ADDRESS, getOrFetchToken} = useTokenModule(); + + type StakingSlice = { + staking: Address | null; + rewardToken?: Awaited>; + rewardRatePerSec?: bigint; + earned?: bigint; + stakedBalance?: bigint; // user staked LP + walletLpBalance?: bigint; // user wallet LP (outside farm) + periodFinish?: bigint; // timestamp (sec) + periodFinishDate?: Date; + + totalStakedLP?: bigint; // farm total staked LP (all users) + totalSupplyLP?: bigint; // LP token total supply + poolReserves?: { token0: Address; token1: Address; reserve0: bigint; reserve1: bigint }; + + poolAprPct?: number; // fees APR (trading fees -> LPs) + epochAprPct?: number; // farming APR (global) + epochAprUserPct?: number; // OPTIONAL: user-specific APR + }; const [stakingMap, setStakingMap] = useState>({}); diff --git a/apps/web/src/features/pool/tableau.module.css b/apps/web/src/features/pool/tableau.module.css index c39e30f24..c7e6ecb96 100644 --- a/apps/web/src/features/pool/tableau.module.css +++ b/apps/web/src/features/pool/tableau.module.css @@ -245,9 +245,38 @@ table { bottom: 2vh; right: 2vh; font-size: 1.2vh; - color: grey; + +} + +.pagination button { + background-color: #313030; + color: var(--text-color); + border: 1px solid rgba(255, 255, 255, 0.08); + padding: 8px 10px; + border-radius: 12px; + font-size: 12px; + cursor: pointer; + transition: + background 0.25s ease, + transform 0.15s ease, + box-shadow 0.2s ease; +} + +.pagination button:hover { + background-color: #313030; + color: var(--text-color); + transform: translateY(-1px); } + +.pagination span { + color: #9d9d9e; + font-size: 12px; + letter-spacing: 0.6px; + padding: 0 6px; +} + + .expiredBadge { display: inline-block; padding: 2px 6px; @@ -368,4 +397,15 @@ table { width: 90%; margin-left: auto; margin-right: auto; +} + +.centerMessage { + display: flex; + justify-content: center; + align-items: center; + padding: 40px 0; + width: 100%; + height: 100%; + text-align: center; + opacity: 0.7; } \ No newline at end of file diff --git a/apps/web/src/features/portfolio/components/PoolPositionsTable.tsx b/apps/web/src/features/portfolio/components/PoolPositionsTable.tsx index c667bfb0c..d7236f2ea 100644 --- a/apps/web/src/features/portfolio/components/PoolPositionsTable.tsx +++ b/apps/web/src/features/portfolio/components/PoolPositionsTable.tsx @@ -1,7 +1,8 @@ import React from "react"; import type { PoolPosition } from "../hooks/usePortfolio"; import styles from "../portfolio.module.css"; -import { getTokenForUI } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + import { getTokenIcon } from "../../../lib/getTokenIcon"; function formatSmart(value: string) { @@ -38,6 +39,7 @@ export function PoolPositionsTable({ data }: { data: PoolPosition[] }) { {data.map((p) => { + const { getTokenForUI } = useTokenModule(); const t0 = getTokenForUI(p.token0.address) ?? p.token0; const t1 = getTokenForUI(p.token1.address) ?? p.token1; const icon0 = getTokenIcon(t0.address ?? ""); diff --git a/apps/web/src/features/portfolio/components/TokenHoldingsTable.tsx b/apps/web/src/features/portfolio/components/TokenHoldingsTable.tsx index 6f8eb6bf0..fcd6c69b8 100644 --- a/apps/web/src/features/portfolio/components/TokenHoldingsTable.tsx +++ b/apps/web/src/features/portfolio/components/TokenHoldingsTable.tsx @@ -1,8 +1,7 @@ -// features/portfolio/components/TokenHoldingsTable.tsx import React from "react"; import type { TokenHolding } from "../hooks/usePortfolio"; import styles from "../portfolio.module.css"; -import { getTokenForUI, NATIVE_PLACEHOLDER } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; import { getTokenIcon } from "../../../lib/getTokenIcon"; function formatSmart(value: string) { @@ -14,6 +13,8 @@ function formatSmart(value: string) { } export function TokenHoldingsTable({ data }: { data: TokenHolding[] }) { + const { getTokenForUI, NATIVE_PLACEHOLDER } = useTokenModule(); + return (

Tokens

@@ -26,17 +27,22 @@ export function TokenHoldingsTable({ data }: { data: TokenHolding[] }) { {data.map((h, i) => { - // Map to UI token (WTTRUST -> tTRUST etc.) - const uiToken = getTokenForUI(h.token.address) ?? h.token; - // Always provide an address for icon: native -> NATIVE_PLACEHOLDER - const addrForIcon = (uiToken.address ?? NATIVE_PLACEHOLDER) as string; + const isNative = !h.token.address; + + // For native token, map to UI token (TRUST / tTRUST) + // For all ERC20 (including wTRUST), keep the raw token from the portfolio hook + const displayToken = isNative + ? getTokenForUI(NATIVE_PLACEHOLDER) ?? h.token + : h.token; + + const addrForIcon = (displayToken.address ?? NATIVE_PLACEHOLDER) as string; const icon = getTokenIcon(addrForIcon); return ( - {uiToken.symbol} - {uiToken.symbol} + {displayToken.symbol} + {displayToken.symbol} {formatSmart(h.balanceFormatted)} diff --git a/apps/web/src/features/portfolio/hooks/usePortfolio.ts b/apps/web/src/features/portfolio/hooks/usePortfolio.ts index 746965bc7..a4f6a8a99 100644 --- a/apps/web/src/features/portfolio/hooks/usePortfolio.ts +++ b/apps/web/src/features/portfolio/hooks/usePortfolio.ts @@ -1,18 +1,12 @@ import { useEffect, useMemo, useState } from "react"; -import type { Address } from "viem"; +import type { Address, Abi } from "viem"; import { erc20Abi, formatUnits } from "viem"; -import { useAccount, usePublicClient } from "wagmi"; -import { abi, addresses } from "@trustswap/sdk"; -import { - TOKENLIST, - toUIList, - getOrFetchToken, - NATIVE_PLACEHOLDER, - isNative, -} from "../../../lib/tokens"; // ← adjust path to your tokens file +import { useAccount, usePublicClient, useChainId } from "wagmi"; +import { abi, getAddresses } from "@trustswap/sdk"; +import { useTokenModule } from "../../../hooks/useTokenModule"; type TokenInfoLite = { - address?: Address; // undefined => native + address?: Address; // undefined => native symbol: string; decimals: number; name?: string; @@ -37,101 +31,159 @@ export type PoolPosition = { amount1Formatted: string; }; -const toAbi = (x: unknown) => (Array.isArray(x) ? x : (x as any)?.abi) as any; +const toAbi = (x: unknown): Abi => + (Array.isArray(x) ? x : (x as any)?.abi) as Abi; + const FACTORY_ABI = toAbi(abi.UniswapV2Factory); const PAIR_ABI = toAbi(abi.UniswapV2Pair); export function usePortfolio() { const { address: account } = useAccount(); - const pc = usePublicClient(); - const [holdings, setHoldings] = useState(null); + const wagmiChainId = useChainId(); + const fallbackChainId = wagmiChainId; + + // Let wagmi give us the right client for the current chain + const pc = usePublicClient({ chainId: fallbackChainId }); + + const [holdings, setHoldings] = useState(null); const [positions, setPositions] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); - // Build UI token list: start from TOKENLIST and hide items flagged as hidden - const uiTokenList = useMemo(() => toUIList(TOKENLIST), []); + const { + TOKENLIST, + toUIList, + getOrFetchToken, + NATIVE_PLACEHOLDER, + isNative, + } = useTokenModule(); + + const uiTokenList = useMemo( + () => toUIList(TOKENLIST), + [TOKENLIST, toUIList], + ); useEffect(() => { if (!pc || !account) return; + + const activeChainId = pc.chain?.id ?? fallbackChainId; + const { UniswapV2Factory } = getAddresses(Number(activeChainId)); + + if (!UniswapV2Factory) { + setError(`No UniswapV2Factory address configured for chainId ${activeChainId}`); + setLoading(false); + return; + } + + const factory = UniswapV2Factory as Address; let cancelled = false; async function run() { try { - if (!pc) return; setLoading(true); setError(null); - // --- 1) Token holdings (native + ERC20 in TOKENLIST) --- - // Native (placeholder in TOKENLIST has isNative=true) - const nativeEntry = TOKENLIST.find(t => t.isNative); - if (!account) throw new Error("Account address is undefined"); + // --- 1) Native balance --- + const nativeEntry = TOKENLIST.find((t) => t.isNative); const nativeBal = await pc.getBalance({ address: account }); const baseHoldings: TokenHolding[] = [ { token: { - // no on-chain address for native; keep undefined symbol: nativeEntry?.symbol ?? "tTRUST", decimals: nativeEntry?.decimals ?? 18, name: nativeEntry?.name, }, balance: nativeBal, - balanceFormatted: formatUnits(nativeBal, nativeEntry?.decimals ?? 18), + balanceFormatted: formatUnits( + nativeBal, + nativeEntry?.decimals ?? 18, + ), }, ]; - // ERC20 balances for UI tokens (exclude native placeholder) - const erc20List = uiTokenList.filter(t => t.address.toLowerCase() !== NATIVE_PLACEHOLDER.toLowerCase()); + // --- 2) ERC20 balances from token list --- + const erc20List = uiTokenList.filter( + (t) => t.address.toLowerCase() !== NATIVE_PLACEHOLDER.toLowerCase(), + ); + const erc20Calls = erc20List.map((t) => ({ address: t.address as Address, abi: erc20Abi, functionName: "balanceOf" as const, args: [account], })); - const erc20Res = erc20Calls.length ? await pc.multicall({ contracts: erc20Calls }) : []; + + const erc20Res = erc20Calls.length + ? await pc.multicall({ contracts: erc20Calls }) + : []; erc20Res.forEach((r, i) => { const meta = erc20List[i]; const bal = r.status === "success" ? (r.result as bigint) : 0n; baseHoldings.push({ - token: { address: meta.address as Address, symbol: meta.symbol, decimals: meta.decimals, name: meta.name }, + token: { + address: meta.address as Address, + symbol: meta.symbol, + decimals: meta.decimals, + name: meta.name, + }, balance: bal, balanceFormatted: formatUnits(bal, meta.decimals), }); }); // Keep native even if zero; filter out zero ERC20s - const nonZero = baseHoldings.filter(h => h.balance > 0n || !h.token.address); + const nonZero = baseHoldings.filter( + (h) => h.balance > 0n || !h.token.address, + ); - // --- 2) Pool positions (scan recent pairs from factory) --- - const factory = addresses.UniswapV2Factory as Address; + // --- 3) Pool positions: scan recent pairs from factory --- const len = (await pc.readContract({ address: factory, abi: FACTORY_ABI, functionName: "allPairsLength", - args: [], })) as bigint; - const MAX_SCAN = 1000n; // adjust to your DEX size + const MAX_SCAN = 1000n; // adjust as needed const scanLen = len > MAX_SCAN ? MAX_SCAN : len; + + if (scanLen === 0n) { + if (!cancelled) { + setHoldings( + nonZero.sort((a, b) => + a.balance === b.balance ? 0 : a.balance < b.balance ? 1 : -1, + ), + ); + setPositions([]); + setLoading(false); + } + return; + } + const start = len - scanLen; - const pairIdxCalls = Array.from({ length: Number(scanLen) }, (_, k) => ({ - address: factory, - abi: FACTORY_ABI, - functionName: "allPairs" as const, - args: [start + BigInt(k)], - })); + const pairIdxCalls = Array.from( + { length: Number(scanLen) }, + (_, k) => ({ + address: factory, + abi: FACTORY_ABI, + functionName: "allPairs" as const, + args: [start + BigInt(k)], + }), + ); + const pairIdxRes = await pc.multicall({ contracts: pairIdxCalls }); const pairAddresses = pairIdxRes - .map(r => (r.status === "success" ? (r.result as Address) : null)) + .map((r) => (r.status === "success" ? (r.result as Address) : null)) .filter(Boolean) as Address[]; if (pairAddresses.length === 0) { if (!cancelled) { setHoldings( - nonZero.sort((a, b) => (a.balance === b.balance ? 0 : a.balance < b.balance ? 1 : -1)) + nonZero.sort((a, b) => + a.balance === b.balance ? 0 : a.balance < b.balance ? 1 : -1, + ), ); setPositions([]); setLoading(false); @@ -179,6 +231,7 @@ export function usePortfolio() { ]); const out: PoolPosition[] = []; + for (let i = 0; i < pairAddresses.length; i++) { const ok = balRes[i]?.status === "success" && @@ -186,38 +239,53 @@ export function usePortfolio() { t0Res[i]?.status === "success" && t1Res[i]?.status === "success" && rsvRes[i]?.status === "success"; + if (!ok) continue; const lpBal = balRes[i].result as bigint; if (lpBal === 0n) continue; const totalSupply = tsRes[i].result as bigint; + if (totalSupply === 0n) continue; + const token0Addr = t0Res[i].result as Address; const token1Addr = t1Res[i].result as Address; - // Parse reserves as tuple: [reserve0, reserve1, blockTimestampLast] - const [reserve0, reserve1] = rsvRes[i].result as readonly [bigint, bigint, number]; + const [reserve0, reserve1] = rsvRes[i].result as readonly [ + bigint, + bigint, + number, + ]; - // Guard against zero totalSupply - if (totalSupply === 0n) continue; - - // Map WTTRUST -> native placeholder for UI, fetch meta safely const [t0Meta, t1Meta] = await Promise.all([ - getOrFetchToken(isNative(token0Addr) ? NATIVE_PLACEHOLDER : token0Addr), - getOrFetchToken(isNative(token1Addr) ? NATIVE_PLACEHOLDER : token1Addr), + getOrFetchToken( + isNative(token0Addr) ? NATIVE_PLACEHOLDER : token0Addr, + ), + getOrFetchToken( + isNative(token1Addr) ? NATIVE_PLACEHOLDER : token1Addr, + ), ]); - // All-bigint math first - const amt0 = (reserve0 * lpBal) / totalSupply; // bigint - const amt1 = (reserve1 * lpBal) / totalSupply; // bigint + const amt0 = (reserve0 * lpBal) / totalSupply; + const amt1 = (reserve1 * lpBal) / totalSupply; - // sharePct as number with 2 decimals (basis points / 100) - const sharePct = Number((lpBal * 10000n) / totalSupply) / 100; + const sharePct = + Number((lpBal * 10000n) / totalSupply) / 100; out.push({ pairAddress: pairAddresses[i], - token0: { address: t0Meta.address, symbol: t0Meta.symbol, decimals: t0Meta.decimals, name: t0Meta.name }, - token1: { address: t1Meta.address, symbol: t1Meta.symbol, decimals: t1Meta.decimals, name: t1Meta.name }, + token0: { + address: t0Meta.address, + symbol: t0Meta.symbol, + decimals: t0Meta.decimals, + name: t0Meta.name, + }, + token1: { + address: t1Meta.address, + symbol: t1Meta.symbol, + decimals: t1Meta.decimals, + name: t1Meta.name, + }, lpBalance: lpBal, lpBalanceFormatted: formatUnits(lpBal, 18), sharePct, @@ -229,7 +297,11 @@ export function usePortfolio() { } if (!cancelled) { - setHoldings(nonZero.sort((a, b) => (a.balance === b.balance ? 0 : a.balance < b.balance ? 1 : -1))); + setHoldings( + nonZero.sort((a, b) => + a.balance === b.balance ? 0 : a.balance < b.balance ? 1 : -1, + ), + ); setPositions(out); setLoading(false); } @@ -242,10 +314,20 @@ export function usePortfolio() { } run(); + return () => { cancelled = true; }; - }, [pc, account, uiTokenList]); + }, [ + pc, + account, + fallbackChainId, + TOKENLIST, + uiTokenList, + getOrFetchToken, + NATIVE_PLACEHOLDER, + isNative, + ]); return { holdings, positions, loading, error }; } diff --git a/apps/web/src/features/swap/components/DetailsDisclosure.tsx b/apps/web/src/features/swap/components/DetailsDisclosure.tsx index 1830c6e99..9d2379871 100644 --- a/apps/web/src/features/swap/components/DetailsDisclosure.tsx +++ b/apps/web/src/features/swap/components/DetailsDisclosure.tsx @@ -11,7 +11,7 @@ export default function DetailsDisclosure({ }: { slippageBps: number; onChangeSlippage: (v: number) => void; - priceText?: string; // ex: "1 WTTRUST ≈ 123.456 TSWP" + priceText?: string; // ex: "1 WTRUST ≈ 123.456 TSWP" priceImpactPct?: number | null; // ex: 0.42 (%) networkFeeText?: string | null; // ex: "0.00087 tTRUST" }) { diff --git a/apps/web/src/features/swap/components/SwapForm.tsx b/apps/web/src/features/swap/components/SwapForm.tsx index 62bb5e2f4..3f5968c30 100644 --- a/apps/web/src/features/swap/components/SwapForm.tsx +++ b/apps/web/src/features/swap/components/SwapForm.tsx @@ -2,12 +2,9 @@ import { useEffect, useMemo, useRef, useState } from "react"; import type { Address } from "viem"; import { getAddress, parseUnits, formatUnits } from "viem"; -import { useAccount, usePublicClient } from "wagmi"; -import { - getDefaultPair, - TOKENLIST, - NATIVE_PLACEHOLDER, -} from "../../../lib/tokens"; +import { useAccount, usePublicClient, useChainId } from "wagmi"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + import { useImportedTokens } from "../hooks/useImportedTokens"; import { useQuoteDetails } from "../hooks/useQuoteDetails"; import { useAllowance } from "../hooks/useAllowance"; @@ -22,10 +19,9 @@ import TokenField from "./TokenField"; import FlipButton from "./FlipButton"; import ApproveAndSwap from "./ApproveAndSwap"; import DetailsDisclosure from "./DetailsDisclosure"; -import { addresses, abi } from "@trustswap/sdk"; +import { addresses as TESTNET_ADDRESSES, abi, getAddresses } from "@trustswap/sdk"; + -const isNative = (a?: Address) => - !!a && a.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); const norm = (a?: string) => (a ? a.toLowerCase() : ""); @@ -66,6 +62,26 @@ type Meta = { export default function SwapForm() { const { address } = useAccount(); const pc = usePublicClient(); + const chainId = useChainId(); + + const { + getDefaultPair, + TOKENLIST, + NATIVE_PLACEHOLDER, + WNATIVE_ADDRESS, + getTokenByAddressOrFallback, +} = useTokenModule(); + + const resolvedChainId = chainId; + const { WTTRUST, TSWP, UniswapV2Router02 } = getAddresses(resolvedChainId); + const WNATIVE = WNATIVE_ADDRESS as Address; + const ROUTER = UniswapV2Router02 as Address; + + const isNative = (a?: Address) => + !!a && a.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); + + const isWrapped = (a?: Address) => + !!a && a.toLowerCase() === WTTRUST.toLowerCase(); const defaults = useMemo(() => getDefaultPair(), []); const [tokenIn, setTokenIn] = useState
(defaults.tokenIn.address); @@ -113,7 +129,8 @@ export default function SwapForm() { }); } return m; - }, [imported]); + }, [TOKENLIST, imported]); + function getMeta(addr?: Address): Meta { if (!addr) { @@ -134,18 +151,19 @@ export default function SwapForm() { } function buildPaths(tin: Address, tout: Address): Address[][] { - const WT = addresses.WTTRUST as Address; - const TSWP = addresses.TSWP as Address; + const WT = WNATIVE as Address; + const tswpAddr = TSWP as Address; const A = isNative(tin) ? WT : tin; const B = isNative(tout) ? WT : tout; - - const paths: Address[][] = [ - [A, B], - [A, WT, B], - [A, TSWP, B], - ]; + const paths: Address[][] = [[A, B], [A, WT, B]]; + + // Optional governance token hop if configured and distinct + const isZero = (addr: Address) => addr.toLowerCase() === "0x0000000000000000000000000000000000000000"; + if (!isZero(tswpAddr) && tswpAddr !== A && tswpAddr !== B) { + paths.push([A, tswpAddr, B]); + } const seen = new Set(); const uniq: Address[][] = []; @@ -159,6 +177,7 @@ export default function SwapForm() { return uniq; } + async function fastRouterQuote( tin: Address, @@ -170,7 +189,7 @@ export default function SwapForm() { const paths = buildPaths(tin, tout); const calls = paths.map(async (path) => { const amounts = (await pc.readContract({ - address: addresses.UniswapV2Router02 as Address, + address: ROUTER, abi: abi.UniswapV2Router02, functionName: "getAmountsOut", args: [amtIn, path], @@ -318,9 +337,13 @@ export default function SwapForm() { tokenOut ?? "0x0000000000000000000000000000000000000000", amountIn, amountOut, - pairData + pairData, + { + NATIVE_PLACEHOLDER, + WNATIVE_ADDRESS, + getTokenByAddressOrFallback, + } ); - // deps: tout ce qui influence l'impact }, [ tokenIn, tokenOut, @@ -335,6 +358,9 @@ export default function SwapForm() { (pairData as any)?.decimals1, bestPath?.join(">"), lastOutBn?.toString(), + NATIVE_PLACEHOLDER, + WNATIVE_ADDRESS, + getTokenByAddressOrFallback, ]); async function onApproveAndSwap() { @@ -343,36 +369,53 @@ export default function SwapForm() { const v = Number(normalizeAmountStr(amountIn)); if (!isFinite(v) || v <= 0) return; - // Réutilise la dernière quote si disponible, sinon hook - const outBn = - lastOutBn ?? - (await (async () => { - const qd = await quoteDetails( - tokenIn, - tokenOut ?? "0x0000000000000000000000000000000000000000", - String(v) - ); - if (!qd) throw new Error("No route/liquidity for this pair"); - return qd.amountOutBn; - })()); - const minOut = outBn - (outBn * BigInt(slippageBps)) / 10_000n; + const isWrap = + !!tokenOut && + isNative(tokenIn) && + isWrapped(tokenOut as Address); + + const isUnwrap = + !!tokenOut && + isWrapped(tokenIn as Address) && + isNative(tokenOut as Address); + + const sameAsset = isWrap || isUnwrap; + + let outBn: bigint; + + if (sameAsset) { + // For wrap/unwrap, 1:1 amount, no routing + const tiMeta = getMeta(tokenIn); + outBn = parseUnits(String(v), tiMeta.decimals); + } else { + // Reuse last quote if available, otherwise recompute with quoteDetails + outBn = + lastOutBn ?? + (await (async () => { + const qd = await quoteDetails( + tokenIn, + tokenOut ?? "0x0000000000000000000000000000000000000000", + String(v) + ); + if (!qd) throw new Error("No route/liquidity for this pair"); + return qd.amountOutBn; + })()); + } + + const minOut = sameAsset + ? outBn // wrap/unwrap is 1:1, no slippage + : outBn - (outBn * BigInt(slippageBps)) / 10_000n; + const deadline = Math.floor(Date.now() / 1000) + 60 * 20; const ti = getMeta(tokenIn); const amtIn = parseUnits(String(v), ti.decimals); - if (!isNative(tokenIn)) { - const curr = await allowance( - address, - tokenIn, - addresses.UniswapV2Router02 as Address - ); + // No allowance/approve for wrap/unwrap, only for router swaps + if (!sameAsset && !isNative(tokenIn)) { + const curr = await allowance(address, tokenIn, ROUTER); if (curr < amtIn) { - await approve( - tokenIn, - addresses.UniswapV2Router02 as Address, - amtIn - ); + await approve(tokenIn, ROUTER, amtIn); } } diff --git a/apps/web/src/features/swap/components/TokenSelector.tsx b/apps/web/src/features/swap/components/TokenSelector.tsx index caa9659fc..91b7f4a78 100644 --- a/apps/web/src/features/swap/components/TokenSelector.tsx +++ b/apps/web/src/features/swap/components/TokenSelector.tsx @@ -3,7 +3,8 @@ import { useState, useRef, useEffect, useMemo } from "react"; import type { Address } from "viem"; import { isAddress, getAddress, erc20Abi } from "viem"; import { usePublicClient } from "wagmi"; -import { TOKENLIST } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + import styles from "@ui/styles/TokenSelector.module.css"; import arrowIcone from "../../../assets/arrow-selector.png"; import deleteIcone from "../../../assets/delete.png"; @@ -51,9 +52,10 @@ export default function TokenSelector({ const pc = usePublicClient(); // Base tokens (props > TOKENLIST) + const { TOKENLIST } = useTokenModule(); const baseTokens: Token[] = useMemo( () => (tokens && tokens.length ? tokens : (TOKENLIST as unknown as Token[])), - [tokens] + [tokens, TOKENLIST] ); // Imported tokens (user) diff --git a/apps/web/src/features/swap/hooks/useAllowance.ts b/apps/web/src/features/swap/hooks/useAllowance.ts index 5da37ddca..bb993d9d7 100644 --- a/apps/web/src/features/swap/hooks/useAllowance.ts +++ b/apps/web/src/features/swap/hooks/useAllowance.ts @@ -1,13 +1,14 @@ import type { Address } from "viem"; import { erc20Abi, maxUint256 } from "viem"; import { usePublicClient } from "wagmi"; -import { NATIVE_PLACEHOLDER } from "../../../lib/tokens"; - -const isNative = (a?: Address) => - !!a && a.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); +import { useTokenModule } from "../../../hooks/useTokenModule"; export function useAllowance() { const pc = usePublicClient(); + const { NATIVE_PLACEHOLDER } = useTokenModule(); + + const isNative = (a?: Address) => + !!a && a.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); return async function allowance( owner: Address, @@ -16,7 +17,6 @@ export function useAllowance() { ): Promise { if (!pc) throw new Error("Public client not available"); - if (isNative(token)) return maxUint256; try { diff --git a/apps/web/src/features/swap/hooks/useDynamicTokenList.ts b/apps/web/src/features/swap/hooks/useDynamicTokenList.ts index 314154c43..262dc84eb 100644 --- a/apps/web/src/features/swap/hooks/useDynamicTokenList.ts +++ b/apps/web/src/features/swap/hooks/useDynamicTokenList.ts @@ -1,12 +1,9 @@ // apps/web/src/features/tokens/useDynamicTokenList.ts import { useEffect, useMemo, useRef, useState } from "react"; import type { Address } from "viem"; -import { - TOKENLIST, - getOrFetchToken, - type TokenInfo, - WNATIVE_ADDRESS, -} from "../../../lib/tokens"; +import { type TokenInfo } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + const low = (s: string) => s.toLowerCase(); const ZERO = "0x0000000000000000000000000000000000000000"; @@ -38,6 +35,7 @@ function pickTokenAddr(p: any, key: "token0" | "token1"): Address | undefined { } export function useDynamicTokenList(rawPools: any) { + const { getOrFetchToken, TOKENLIST, WNATIVE_ADDRESS } = useTokenModule(); const base = useMemo(() => TOKENLIST, []); const [tokens, setTokens] = useState(base); const inFlight = useRef>(new Set()); @@ -91,7 +89,7 @@ export function useDynamicTokenList(rawPools: any) { for (const f of fetched) { if (f && !map.has(low(f.address))) { - const hidden = low(f.address) === low(WNATIVE_ADDRESS); // masque WTTRUST si besoin + const hidden = low(f.address) === low(WNATIVE_ADDRESS); // masque WTRUST si besoin map.set(low(f.address), hidden ? { ...f, hidden: true } : f); } } diff --git a/apps/web/src/features/swap/hooks/useGasEstimate.ts b/apps/web/src/features/swap/hooks/useGasEstimate.ts index 8123afdd5..7e74e73f8 100644 --- a/apps/web/src/features/swap/hooks/useGasEstimate.ts +++ b/apps/web/src/features/swap/hooks/useGasEstimate.ts @@ -3,7 +3,8 @@ import type { Address } from "viem"; import { usePublicClient } from "wagmi"; import { abi, addresses } from "@trustswap/sdk"; import { formatUnits } from "viem"; -import { buildPath } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + type Args = { account?: Address; @@ -25,6 +26,7 @@ export function useGasEstimate() { if (!args.account) return null; if (!args.path || args.path.length < 2) return null; + const { buildPath } = useTokenModule(); const path = buildPath(args.path); const symbol = args.nativeSymbol ?? "tTRUST"; const nativeIn = !!args.nativeIn; diff --git a/apps/web/src/features/swap/hooks/usePairData.ts b/apps/web/src/features/swap/hooks/usePairData.ts index deae1a8dd..f713bd6cd 100644 --- a/apps/web/src/features/swap/hooks/usePairData.ts +++ b/apps/web/src/features/swap/hooks/usePairData.ts @@ -1,7 +1,8 @@ import type { Address } from "viem"; import { usePublicClient } from "wagmi"; import { abi, addresses } from "@trustswap/sdk"; -import { toWrapped } from "../../../lib/tokens"; +import { useTokenModule } from "../../../hooks/useTokenModule"; + export type PairData = { pair: Address; @@ -15,12 +16,15 @@ const ZERO: Address = "0x0000000000000000000000000000000000000000"; export function usePairData() { const pc = usePublicClient(); - + const { toWrapped } = useTokenModule(); + return async function fetchPair(tokenA: Address, tokenB: Address): Promise { if (!pc) return null; if (!tokenA || !tokenB) return null; if (tokenA.toLowerCase() === tokenB.toLowerCase()) return null; + + // ⚠️ wrap natif -> WNATIVE avant d’interroger la factory const a = toWrapped(tokenA); const b = toWrapped(tokenB); diff --git a/apps/web/src/features/swap/hooks/usePriceImpact.ts b/apps/web/src/features/swap/hooks/usePriceImpact.ts index 89c30faf6..aff678a78 100644 --- a/apps/web/src/features/swap/hooks/usePriceImpact.ts +++ b/apps/web/src/features/swap/hooks/usePriceImpact.ts @@ -1,14 +1,9 @@ // usePriceImpact.ts import type { Address } from "viem"; import type { PairData } from "./usePairData"; -import { NATIVE_PLACEHOLDER, WNATIVE_ADDRESS, getTokenByAddressOrFallback } from "../../../lib/tokens"; +import type { TokenInfo } from "../../../lib/tokens"; import { formatUnits } from "viem"; -const wrap = (a: Address) => - a?.toLowerCase?.() === NATIVE_PLACEHOLDER.toLowerCase() - ? (WNATIVE_ADDRESS as Address) - : a; - const num = (x: string | number | undefined) => { const n = Number(String(x ?? "").replace(",", ".").trim()); return Number.isFinite(n) ? n : 0; @@ -18,15 +13,17 @@ const toBi = (x: unknown): bigint | null => { try { if (typeof x === "bigint") return x; if (typeof x === "number") return BigInt(Math.trunc(x)); - if (typeof x === "string") return BigInt(x); // support "123" / "0x..." + if (typeof x === "string") return BigInt(x); + return null; + } catch { return null; - } catch { return null; } + } }; const safeUnits = (v: bigint, decimals: number): number => { - try { return Number(formatUnits(v, decimals)); } - catch { - // fallback naïf si formatUnits échoue (rare) + try { + return Number(formatUnits(v, decimals)); + } catch { const s = v.toString(); if (decimals <= 0) return Number(s); const len = s.length; @@ -36,34 +33,43 @@ const safeUnits = (v: bigint, decimals: number): number => { } }; +export type PriceImpactHelpers = { + NATIVE_PLACEHOLDER: Address; + WNATIVE_ADDRESS: Address; + getTokenByAddressOrFallback: (addr: Address) => TokenInfo; +}; + export function computePriceImpactPct( tokenIn: Address, tokenOut: Address, amountInStr: string, amountOutStr: string, - pair: PairData | null + pair: PairData | null, + helpers: PriceImpactHelpers ): number | null { - // 0) montants - const ain = num(amountInStr); + const ain = num(amountInStr); const aout = num(amountOutStr); - if (ain <= 0 || aout <= 0) return null; // pas assez d'info + if (ain <= 0 || aout <= 0) return null; - // 1) pair présente if (!pair) return null; - // 2) map adresses (wrap natif) - const inAsPair = wrap(tokenIn)?.toLowerCase?.(); + const { NATIVE_PLACEHOLDER, WNATIVE_ADDRESS, getTokenByAddressOrFallback } = helpers; + + const wrap = (a: Address) => + a?.toLowerCase?.() === NATIVE_PLACEHOLDER.toLowerCase() + ? (WNATIVE_ADDRESS as Address) + : a; + + const inAsPair = wrap(tokenIn)?.toLowerCase?.(); const outAsPair = wrap(tokenOut)?.toLowerCase?.(); const t0 = (pair as any).token0?.toLowerCase?.(); const t1 = (pair as any).token1?.toLowerCase?.(); if (!inAsPair || !outAsPair || !t0 || !t1) return null; - // si la pair ne correspond pas à la sélection courante → null propre if (!((inAsPair === t0 || inAsPair === t1) && (outAsPair === t0 || outAsPair === t1))) { return null; } - // 3) réserves + décimales (tolérant : fallback 18 si manquant) const r0bi = toBi((pair as any).reserve0); const r1bi = toBi((pair as any).reserve1); if (r0bi === null || r1bi === null) return null; @@ -82,20 +88,17 @@ export function computePriceImpactPct( const R1 = safeUnits(r1bi, d1); if (R0 <= 0 || R1 <= 0 || !isFinite(R0) || !isFinite(R1)) return null; - // 4) choisir le bon sens const inIs0 = inAsPair === t0; - const rIn = inIs0 ? R0 : R1; + const rIn = inIs0 ? R0 : R1; const rOut = inIs0 ? R1 : R0; if (rIn <= 0 || rOut <= 0) return null; - // 5) spot vs execution const spot = rOut / rIn; const exec = aout / ain; if (!isFinite(spot) || !isFinite(exec) || spot <= 0 || exec <= 0) return null; let impact = ((spot - exec) / spot) * 100; - // 6) bornes "sanity" (évite les 998%) if (!isFinite(impact)) return null; if (impact < 0) impact = 0; if (impact > 100) impact = 100; diff --git a/apps/web/src/features/swap/hooks/useQuoteDetails.ts b/apps/web/src/features/swap/hooks/useQuoteDetails.ts index e86b912a6..4479f90cb 100644 --- a/apps/web/src/features/swap/hooks/useQuoteDetails.ts +++ b/apps/web/src/features/swap/hooks/useQuoteDetails.ts @@ -1,23 +1,33 @@ import type { Address } from "viem"; import { parseUnits, formatUnits } from "viem"; import { usePublicClient } from "wagmi"; -import { getTokenByAddress, NATIVE_PLACEHOLDER } from "../../../lib/tokens"; -import { abi, addresses } from "@trustswap/sdk"; +import { abi } from "@trustswap/sdk"; +import { useTokenModule } from "../../../hooks/useTokenModule"; +import { useTrustswapAddresses } from "../../../hooks/useTrustswapAddresses"; -const ROUTER = addresses.UniswapV2Router02 as Address; -const WNATIVE = addresses.WTTRUST as Address; - -const isNative = (a?: Address) => - !!a && a.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); -const toWrapped = (a: Address) => (isNative(a) ? WNATIVE : a); - -function getDecimalsSafe(addr: Address) { +function getDecimalsSafe( + addr: Address, + isNative: (a?: Address) => boolean, + getTokenByAddress: (addr: Address) => { decimals: number } +) { if (isNative(addr)) return 18; - try { return getTokenByAddress(addr).decimals ?? 18; } catch { return 18; } + try { + return getTokenByAddress(addr).decimals ?? 18; + } catch { + return 18; + } } export function useQuoteDetails() { const pc = usePublicClient(); + const { UniswapV2Router02 } = useTrustswapAddresses(); + + const { + NATIVE_PLACEHOLDER, + WNATIVE_ADDRESS, + isNative, + getTokenByAddress, + } = useTokenModule(); return async function getQuoteDetails( tokenIn: Address, @@ -30,31 +40,44 @@ export function useQuoteDetails() { decimalsOut: number; } | null> { if (!pc) return null; + const v = Number(String(amountInStr).replace(",", ".")); if (!isFinite(v) || v <= 0) return null; - const decimalsIn = getDecimalsSafe(tokenIn); - const decimalsOut = getDecimalsSafe(tokenOut); + const decimalsIn = getDecimalsSafe(tokenIn, isNative, getTokenByAddress); + const decimalsOut = getDecimalsSafe(tokenOut, isNative, getTokenByAddress); const amountIn = parseUnits(String(v), decimalsIn); - const direct = [toWrapped(tokenIn), toWrapped(tokenOut)] as Address[]; - const viaW = [toWrapped(tokenIn), WNATIVE, toWrapped(tokenOut)] as Address[]; + const router = UniswapV2Router02 as Address; + const wNative = WNATIVE_ADDRESS as Address; + + const nativeEq = (a?: Address) => + !!a && a.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); + + const wrap = (a: Address) => + nativeEq(a) || isNative(a) ? wNative : a; + + const direct = [wrap(tokenIn), wrap(tokenOut)] as Address[]; + const viaW = [wrap(tokenIn), wNative, wrap(tokenOut)] as Address[]; const candidates: Address[][] = []; - const same = direct.length === viaW.length && direct.every((x, i) => x === viaW[i]); + const same = + direct.length === viaW.length && direct.every((x, i) => x === viaW[i]); candidates.push(direct); if (!same) candidates.push(viaW); const tryPath = async (path: Address[]) => { try { - const amounts = await pc.readContract({ - address: ROUTER, + const amounts = (await pc.readContract({ + address: router, abi: abi.UniswapV2Router02, functionName: "getAmountsOut", args: [amountIn, path], - }) as bigint[]; + })) as bigint[]; return { out: amounts[amounts.length - 1], path }; - } catch { return null; } + } catch { + return null; + } }; const quotes = await Promise.all(candidates.map(tryPath)); @@ -62,6 +85,7 @@ export function useQuoteDetails() { if (!valid.length) return null; const best = valid.sort((a, b) => (a.out < b.out ? 1 : -1))[0]; + return { amountOutFormatted: formatUnits(best.out, decimalsOut), amountOutBn: best.out, diff --git a/apps/web/src/features/swap/hooks/useSwap.ts b/apps/web/src/features/swap/hooks/useSwap.ts index 50335aba7..a384adfa6 100644 --- a/apps/web/src/features/swap/hooks/useSwap.ts +++ b/apps/web/src/features/swap/hooks/useSwap.ts @@ -1,30 +1,15 @@ import type { Address } from "viem"; import { erc20Abi, parseUnits, formatUnits } from "viem"; import { usePublicClient, useWalletClient, useChainId } from "wagmi"; -import { getOrFetchToken } from "../../../lib/tokens"; -import { abi, addresses } from "@trustswap/sdk"; +import { useTokenModule } from "../../../hooks/useTokenModule"; +import { abi, getAddresses } from "@trustswap/sdk"; import { useAlerts } from "../../../features/alerts/Alerts"; -const NATIVE_PLACEHOLDER = addresses.NATIVE_PLACEHOLDER as Address; // tTRUST (pseudo "native") -const WNATIVE = addresses.WTTRUST as Address; // WTTRUST (wrapped) -const ROUTER = addresses.UniswapV2Router02 as Address; - -const isNative = (addr?: Address) => - !!addr && addr.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); - -const isWrapped = (addr?: Address) => - !!addr && addr.toLowerCase() === WNATIVE.toLowerCase(); - -const eqAddr = (a?: Address, b?: Address) => - !!a && !!b && a.toLowerCase() === b.toLowerCase(); - -const toWrapped = (addr: Address) => (isNative(addr) ? WNATIVE : addr); -const buildPath = (path: Address[]) => path.map(toWrapped) as Address[]; - function explorerTx(chainId: number | undefined, hash?: `0x${string}`) { if (!hash) return undefined; const map: Record = { 13579: "https://testnet.explorer.intuition.systems/tx/", + 1155: "https://explorer.intuition.systems/tx/", }; const base = map[chainId ?? 0]; return base ? `${base}${hash}` : undefined; @@ -32,9 +17,12 @@ function explorerTx(chainId: number | undefined, hash?: `0x${string}`) { function prettifySwapError(err: any): string { const raw = - String(err?.shortMessage || "") + " | " + - String(err?.message || "") + " | " + - String(err?.cause?.shortMessage || "") + " | " + + String(err?.shortMessage || "") + + " | " + + String(err?.message || "") + + " | " + + String(err?.cause?.shortMessage || "") + + " | " + String(err?.cause?.message || ""); const msg = raw.toLowerCase(); @@ -44,7 +32,8 @@ function prettifySwapError(err: any): string { msg.includes("request rejected") || msg.includes("action rejected") || String(err?.code) === "4001" - ) return "Transaction rejected by user."; + ) + return "Transaction rejected by user."; if (raw.includes("TransferHelper::transferFrom: transferFrom failed")) return "Insufficient allowance or balance for the input token."; @@ -53,7 +42,8 @@ function prettifySwapError(err: any): string { raw.includes("INSUFFICIENT_OUTPUT_AMOUNT") || raw.includes("ExcessiveInputAmount") || msg.includes("insufficient output") - ) return "Slippage too low (insufficient output)."; + ) + return "Slippage too low (insufficient output)."; if (msg.includes("deadline") || msg.includes("expired")) return "Transaction deadline exceeded."; @@ -61,11 +51,9 @@ function prettifySwapError(err: any): string { if (raw.includes("Transfers restricted") || msg.includes("transfer restricted")) return "Token has transfer restrictions."; - if (msg.includes("insufficient funds for gas")) - return "Insufficient funds for gas."; + if (msg.includes("insufficient funds for gas")) return "Insufficient funds for gas."; - if (msg.includes("nonce too low")) - return "Nonce too low."; + if (msg.includes("nonce too low")) return "Nonce too low."; if (msg.includes("replacement transaction underpriced") || msg.includes("fee too low")) return "Replacement transaction underpriced."; @@ -78,17 +66,34 @@ export function useSwap() { const chainId = useChainId(); const alerts = useAlerts(); + const { + NATIVE_PLACEHOLDER, + WNATIVE_ADDRESS, + isNative, + isWrapped, + toWrapped, + buildPath, + getOrFetchToken, + } = useTokenModule(); + + const resolvedChainId = chainId; + const { UniswapV2Router02 } = getAddresses(resolvedChainId); + const router = UniswapV2Router02 as Address; + const nativePlaceholder = NATIVE_PLACEHOLDER as Address; + const wNative = WNATIVE_ADDRESS as Address; + + const eqAddr = (a?: Address, b?: Address) => + !!a && !!b && a.toLowerCase() === b.toLowerCase(); + const approveIfNeeded = async (token: Address, owner: Address, amount: bigint) => { if (!publicClient || !wallet) throw new Error("Wallet not connected"); - if (isNative(token)) return; // no approve for pseudo-native - if (isWrapped(token)) return; // no approve needed for wrap/unwrap flows - + if (isNative(token)) return; // do not approve native const allowance = (await publicClient.readContract({ address: token, abi: erc20Abi, functionName: "allowance", - args: [owner, ROUTER], + args: [owner, router], })) as bigint; if (allowance >= amount) return; @@ -98,7 +103,7 @@ export function useSwap() { address: token, abi: erc20Abi, functionName: "approve", - args: [ROUTER, amount], + args: [router, amount], }); alerts.push({ @@ -146,15 +151,14 @@ export function useSwap() { if (!wallet) throw new Error("Wallet not connected"); if (!publicClient) throw new Error("No public client"); if (!tokenIn || !tokenOut) throw new Error("Missing token addresses"); - // Guard unsupported flows: TOKEN -> WTTRUST without a pool + + // Guard: direct non-native -> wrapped without pool if (isWrapped(tokenOut) && !isNative(tokenIn)) { throw new Error( - "Cannot swap directly to WTTRUST from non-native token. Swap to tTRUST first, then wrap to WTTRUST." + "Cannot swap directly to wrapped native from non-native token. Swap to native first, then wrap." ); } - - // Allow native <-> wrapped as a valid "pair" even if addresses are effectively the same asset const sameAsset = (isNative(tokenIn) && isWrapped(tokenOut)) || (isWrapped(tokenIn) && isNative(tokenOut)); @@ -167,7 +171,6 @@ export function useSwap() { throw new Error("Native-to-native swap is not supported"); } - // Resolve decimals for input const [tIn, tOut] = await Promise.all([ getOrFetchToken(toWrapped(tokenIn)), getOrFetchToken(toWrapped(tokenOut)), @@ -181,30 +184,28 @@ export function useSwap() { try { let hash: `0x${string}`; - // ===== Wrap / Unwrap special-cases (no router) ===== + // Wrap / unwrap (direct calls to wrapped native contract) if (isNative(tokenIn) && isWrapped(tokenOut)) { - // Wrap: deposit native into WTTRUST hash = await wallet.writeContract({ - address: WNATIVE, - abi: abi.WTTRUST, // must include deposit()/withdraw(uint256) + address: wNative, + abi: abi.WTTRUST, // must be the wrapper ABI functionName: "deposit", args: [], value: amountIn, }); } else if (isWrapped(tokenIn) && isNative(tokenOut)) { - // Unwrap: withdraw WTTRUST back to native hash = await wallet.writeContract({ - address: WNATIVE, + address: wNative, abi: abi.WTTRUST, functionName: "withdraw", args: [amountIn], }); } - // ===== Router swaps (classic) ===== + // Router swaps else if (isNative(tokenIn) && !isNative(tokenOut)) { const path = buildPath([tokenIn, tokenOut]); hash = await wallet.writeContract({ - address: ROUTER, + address: router, abi: abi.UniswapV2Router02, functionName: "swapExactETHForTokens", args: [minOut, path, owner, deadline], @@ -214,7 +215,7 @@ export function useSwap() { const path = buildPath([tokenIn, tokenOut]); await approveIfNeeded(tokenIn, owner, amountIn); hash = await wallet.writeContract({ - address: ROUTER, + address: router, abi: abi.UniswapV2Router02, functionName: "swapExactTokensForETH", args: [amountIn, minOut, path, owner, deadline], @@ -223,7 +224,7 @@ export function useSwap() { const path = buildPath([tokenIn, tokenOut]); await approveIfNeeded(tokenIn, owner, amountIn); hash = await wallet.writeContract({ - address: ROUTER, + address: router, abi: abi.UniswapV2Router02, functionName: "swapExactTokensForTokens", args: [amountIn, minOut, path, owner, deadline], @@ -241,9 +242,9 @@ export function useSwap() { const receipt = await publicClient.waitForTransactionReceipt({ hash }); - // Friendly success message - const labelIn = isNative(tokenIn) ? "tTRUST" : (tIn.symbol || "TOKEN"); - const labelOut = isNative(tokenOut) ? "tTRUST" : (tOut.symbol || "TOKEN"); + const labelIn = isNative(tokenIn) ? "TRUST" : tIn.symbol || "TOKEN"; + const labelOut = + isNative(tokenOut) ? "TRUST" : isWrapped(tokenOut) ? "wTRUST" : tOut.symbol || "TOKEN"; const shownMinOut = formatUnits(minOut, Number(tOut.decimals ?? 18)); alerts.push({ diff --git a/apps/web/src/features/swap/hooks/useTokenBalance.ts b/apps/web/src/features/swap/hooks/useTokenBalance.ts index ce8da0896..449ff1cf0 100644 --- a/apps/web/src/features/swap/hooks/useTokenBalance.ts +++ b/apps/web/src/features/swap/hooks/useTokenBalance.ts @@ -3,13 +3,10 @@ import type { Address } from "viem"; import { erc20Abi, formatUnits, zeroAddress } from "viem"; import { useAccount, usePublicClient } from "wagmi"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { - isNative as isNativeAddr, - NATIVE_PLACEHOLDER, - getOrFetchToken, // ✅ utilise ce helper on-chain - WNATIVE_ADDRESS, - type TokenInfo, -} from "../../../lib/tokens"; +import { type TokenInfo } from "../../../lib/tokens"; + +import { useTokenModule } from "../../../hooks/useTokenModule"; + import { useLiveRegister } from "../../../live/LiveRefetchProvider"; type Result = { @@ -26,6 +23,7 @@ export function useTokenBalance(token?: Address, owner?: Address): Result { const { chain } = useAccount(); const pc = usePublicClient(); const isUnsetToken = !token || token.toLowerCase() === zeroAddress; + const { NATIVE_PLACEHOLDER, getOrFetchToken, isNative } = useTokenModule(); const [state, setState] = useState({ isLoading: !!token && !!owner, @@ -61,14 +59,14 @@ export function useTokenBalance(token?: Address, owner?: Address): Result { } try { - // ✅ récupère meta on-chain si besoin (ne throw pas si hors TOKENLIST) - const meta = isNativeAddr(token!) + // récupère meta on-chain si besoin (ne throw pas si hors TOKENLIST) + const meta = isNative(token!) ? nativeMeta : await getOrFetchToken(token!); let raw: bigint = 0n; - if (meta.isNative || isNativeAddr(token!)) { + if (meta.isNative || isNative(token!)) { raw = await pc.getBalance({ address: owner }); } else { // balanceOf peut revert sur des tokens “non standard” → protège diff --git a/apps/web/src/hooks/useTokenMeta.ts b/apps/web/src/hooks/useTokenMeta.ts index 0bc402cc0..9505ee867 100644 --- a/apps/web/src/hooks/useTokenMeta.ts +++ b/apps/web/src/hooks/useTokenMeta.ts @@ -1,51 +1,48 @@ -// features/shared/hooks/useTokenMeta.ts +// web/src/hooks/useTokenMeta.ts import { useEffect, useState } from "react"; import type { Address } from "viem"; -import { getOrFetchToken, isNative, NATIVE_PLACEHOLDER } from "../lib/tokens"; +import { useTokenModule } from "./useTokenModule"; export type UIMeta = { - address: Address; // adresse "UI" (placeholder si natif) + address: Address; symbol: string; name?: string; - decimals: number; // 18 si natif - isNative?: boolean; // true si tTRUST + decimals: number; + isNative?: boolean; }; export function useTokenMeta(addr?: Address) { - const [state, setState] = useState<{ meta?: UIMeta; loading: boolean; error?: unknown }>({ + const { getTokenMetaSafe } = useTokenModule(); + + const [state, setState] = useState<{ + meta?: UIMeta; + loading: boolean; + error?: unknown; + }>({ loading: !!addr, }); useEffect(() => { let alive = true; + (async () => { - if (!addr) { if (alive) setState({ loading: false }); return; } + if (!addr) { + if (alive) setState({ loading: false }); + return; + } try { - if (isNative(addr)) { - if (!alive) return; - setState({ - loading: false, - meta: { - address: NATIVE_PLACEHOLDER, - symbol: "tTRUST", - name: "Native TRUST", - decimals: 18, - isNative: true, - }, - }); - return; - } - - const onchain = await getOrFetchToken(addr); + const info = await getTokenMetaSafe(addr); if (!alive) return; + setState({ loading: false, meta: { - address: addr, - symbol: onchain.symbol, - name: onchain.name, - decimals: Number(onchain.decimals ?? 18), + address: info.address, + symbol: info.symbol, + name: info.name, + decimals: info.decimals, + isNative: info.isNative, }, }); } catch (error) { @@ -54,8 +51,10 @@ export function useTokenMeta(addr?: Address) { } })(); - return () => { alive = false; }; - }, [addr]); + return () => { + alive = false; + }; + }, [addr, getTokenMetaSafe]); return state; } diff --git a/apps/web/src/hooks/useTokenModule.ts b/apps/web/src/hooks/useTokenModule.ts new file mode 100644 index 000000000..c29d9e1ff --- /dev/null +++ b/apps/web/src/hooks/useTokenModule.ts @@ -0,0 +1,24 @@ +import { useMemo } from "react"; +import { useChainId } from "wagmi"; +import type { Chain } from "viem"; +import { intuitionTestnet, intuitionMainnet } from "@trustswap/sdk"; +import { getAddresses } from "@trustswap/sdk"; +import { createTokenModule } from "../lib/tokens"; + + +const CHAINS_BY_ID: Record = { + [intuitionTestnet.id]: intuitionTestnet as unknown as Chain, + [intuitionMainnet.id]: intuitionMainnet as unknown as Chain, +}; + +export function useTokenModule() { + const chainId = useChainId() ?? intuitionTestnet.id; + + const chain = CHAINS_BY_ID[chainId] ?? CHAINS_BY_ID[intuitionTestnet.id]; + const addrBook = getAddresses(chainId); + + return useMemo( + () => createTokenModule(chain, addrBook), + [chain, addrBook], + ); +} diff --git a/apps/web/src/hooks/useTrustswapAddresses.ts b/apps/web/src/hooks/useTrustswapAddresses.ts new file mode 100644 index 000000000..c05569aa1 --- /dev/null +++ b/apps/web/src/hooks/useTrustswapAddresses.ts @@ -0,0 +1,10 @@ +// src/hooks/useTrustswapAddresses.ts +import { useChainId } from "wagmi"; +import { getAddresses } from "@trustswap/sdk"; + +const FALLBACK_CHAIN_ID = 13579; + +export function useTrustswapAddresses() { + const chainId = useChainId() || FALLBACK_CHAIN_ID; + return getAddresses(chainId); +} diff --git a/apps/web/src/lib/dynamic.tsx b/apps/web/src/lib/dynamic.tsx index b424a5154..573a1962c 100644 --- a/apps/web/src/lib/dynamic.tsx +++ b/apps/web/src/lib/dynamic.tsx @@ -1,37 +1,48 @@ -import type { PropsWithChildren } from "react" -import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core" -import { EthereumWalletConnectors } from "@dynamic-labs/ethereum" -import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector" -import { WagmiProvider } from "wagmi" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { wagmiConfig } from "./wagmi" -import { INTUITION } from "@trustswap/sdk" +import type { PropsWithChildren } from "react"; +import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core"; +import { EthereumWalletConnectors } from "@dynamic-labs/ethereum"; +import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector"; +import { WagmiProvider } from "wagmi"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -const queryClient = new QueryClient() +import { wagmiConfig } from "./wagmi"; +import { intuitionTestnet, intuitionMainnet } from "@trustswap/sdk"; -function toDynamicEvmNetwork() { +const queryClient = new QueryClient(); + +function toDynamicEvmNetwork(chain: typeof intuitionTestnet | typeof intuitionMainnet, opts: { testnet: boolean }) { return { - chainId: INTUITION.id, - networkId: INTUITION.id, - name: INTUITION.name, + chainId: chain.id, + networkId: chain.id, + name: chain.name, vanityName: "Intuition", shortName: "intuition", - chainName: INTUITION.name, - rpcUrls: INTUITION.rpcUrls.default.http.slice(), - blockExplorerUrls: [INTUITION.blockExplorers?.default?.url].filter(Boolean) as string[], - nativeCurrency: INTUITION.nativeCurrency, - testnet: true, + chainName: chain.name, + rpcUrls: chain.rpcUrls.default.http.slice(), + blockExplorerUrls: [chain.blockExplorers?.default?.url].filter(Boolean) as string[], + nativeCurrency: chain.nativeCurrency, + testnet: opts.testnet, iconUrls: [], - } + }; } +const dynamicEvmNetworks = [ + toDynamicEvmNetwork(intuitionTestnet, { testnet: true }), + toDynamicEvmNetwork(intuitionMainnet, { testnet: false }), +]; + export function RootProviders({ children }: PropsWithChildren) { - // Prefer env var, fallback to hard-coded id - const envId = (import.meta.env.VITE_DYNAMIC_ENV_ID as string | undefined) ?? "78601171-b1f9-42d1-b651-b76f97becab7" + const envId = + (import.meta.env.VITE_DYNAMIC_ENV_ID as string | undefined) ?? + "78601171-b1f9-42d1-b651-b76f97becab7"; if (!envId) { - console.error("Missing Dynamic environmentId") - return
Wallet connect disabled: missing Dynamic environmentId.
+ console.error("Missing Dynamic environmentId"); + return ( +
+ Wallet connect disabled: missing Dynamic environmentId. +
+ ); } return ( @@ -39,7 +50,7 @@ export function RootProviders({ children }: PropsWithChildren) { settings={{ environmentId: envId, walletConnectors: [EthereumWalletConnectors], - overrides: { evmNetworks: [toDynamicEvmNetwork()] }, + overrides: { evmNetworks: dynamicEvmNetworks }, }} > @@ -48,5 +59,5 @@ export function RootProviders({ children }: PropsWithChildren) { - ) + ); } diff --git a/apps/web/src/lib/erc20Read.ts b/apps/web/src/lib/erc20Read.ts index 5096ad328..ddc7a0857 100644 --- a/apps/web/src/lib/erc20Read.ts +++ b/apps/web/src/lib/erc20Read.ts @@ -1,14 +1,20 @@ -// lib/erc20Read.ts +// src/lib/useErc20Read.ts import type { Address } from "viem"; -import { NATIVE_PLACEHOLDER, WNATIVE_ADDRESS } from "./tokens"; // ajuste le chemin +import { useTokenModule } from "../hooks/useTokenModule"; -export function toERC20ForRead(addr?: Address): Address | undefined { - if (!addr) return undefined; - return addr.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase() - ? (WNATIVE_ADDRESS as Address) - : addr; -} +export function useErc20Read() { + const { NATIVE_PLACEHOLDER, WNATIVE_ADDRESS } = useTokenModule(); + + function toERC20ForRead(addr?: Address): Address | undefined { + if (!addr) return undefined; + return addr.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase() + ? (WNATIVE_ADDRESS as Address) + : addr; + } + + function isZeroAddress(addr: Address) { + return addr === "0x0000000000000000000000000000000000000000"; + } -export function isZeroAddress(addr: Address) { - return addr === "0x0000000000000000000000000000000000000000"; + return { toERC20ForRead, isZeroAddress }; } diff --git a/apps/web/src/lib/tokenFilters.ts b/apps/web/src/lib/tokenFilters.ts index 60d1cb695..5c26b4006 100644 --- a/apps/web/src/lib/tokenFilters.ts +++ b/apps/web/src/lib/tokenFilters.ts @@ -19,7 +19,7 @@ const DENY_TOKEN_ADDRESSES: string[] = [ "0x124c4e8470ed201ae896c2df6ee7152ab7438d80", "0x5fdd4edd250b9214d77103881be0f09812d501d6", - "0x51379cc2c942ee2ae2ff0bd67a7b475f0be39dcf", + "0x51379cc2c942ee2ae2ff0bd67a7b475f0be39dcf", ]; const DENY_SYMBOLS: string[] = [ diff --git a/apps/web/src/lib/tokenIcons.ts b/apps/web/src/lib/tokenIcons.ts index f7be24278..485378896 100644 --- a/apps/web/src/lib/tokenIcons.ts +++ b/apps/web/src/lib/tokenIcons.ts @@ -6,5 +6,6 @@ import { addresses } from "@trustswap/sdk"; export const tokenIcons: Record = { [addresses.NATIVE_PLACEHOLDER]: tTrustIcon, "0xc82d6A5e0Da8Ce7B37330C4D44E9f069269546E6": tTrustIcon, + "0x81cFb09cb44f7184Ad934C09F82000701A4bF672" : tTrustIcon, "0x7da120065e104C085fAc6f800d257a6296549cF3": tswp, }; diff --git a/apps/web/src/lib/tokens.ts b/apps/web/src/lib/tokens.ts index bc88b6329..8d3ccc5cb 100644 --- a/apps/web/src/lib/tokens.ts +++ b/apps/web/src/lib/tokens.ts @@ -1,10 +1,6 @@ -import type { Address } from "viem"; -import { INTUITION, addresses } from "@trustswap/sdk"; -import { createPublicClient, http, erc20Abi } from "viem"; - -export const NATIVE_PLACEHOLDER = addresses.NATIVE_PLACEHOLDER as Address; -export const WNATIVE_ADDRESS = addresses.WTTRUST as Address; - +import type { Address, Chain } from "viem"; +import type { Addresses } from "@trustswap/sdk"; +import { createPublicClient, http, erc20Abi, zeroAddress } from "viem"; export type TokenInfo = { address: Address; @@ -12,139 +8,205 @@ export type TokenInfo = { name: string; decimals: number; isNative?: boolean; - hidden?: boolean; // 👈 ajouté + hidden?: boolean; }; -export const TOKENLIST: TokenInfo[] = [ - { +type TokenModule = { + NATIVE_PLACEHOLDER: Address; + WNATIVE_ADDRESS: Address; + TOKENLIST: TokenInfo[]; + isNative: (addr?: string) => boolean; + isWrapped: (addr?: string) => boolean; + toWrapped: (addr: Address) => Address; + buildPath: (path: Address[]) => Address[]; + getTokenByAddress: (addr: string | Address) => TokenInfo; + getTokenByAddressOrFallback: (addr: Address) => TokenInfo; + getDefaultPair: () => { tokenIn: TokenInfo; tokenOut: TokenInfo }; + getOrFetchToken: (address: Address) => Promise; + getTokenMetaSafe: (addr: Address) => Promise; + toUIAddress: (addr?: Address) => Address | undefined; + toUIList: (list: TokenInfo[]) => TokenInfo[]; + getTokenForUI: (addr?: Address) => TokenInfo | null; +}; + +export function createTokenModule(chain: Chain, addrBook: Addresses): TokenModule { + const NATIVE_PLACEHOLDER = addrBook.NATIVE_PLACEHOLDER as Address; + const WT = (addrBook.WTTRUST ?? zeroAddress) as Address; + const TSWP_ADDR = (addrBook.TSWP ?? zeroAddress) as Address; + + const isZero = (addr?: string) => + !addr || addr.toLowerCase() === zeroAddress; + + const WNATIVE_ADDRESS = WT; + + const baseTokens: TokenInfo[] = []; + + // Native pseudo-token + baseTokens.push({ address: NATIVE_PLACEHOLDER, - symbol: INTUITION?.nativeCurrency?.symbol ?? "tTRUST", - name: INTUITION?.nativeCurrency?.name ?? "Native TRUST", + symbol: chain.nativeCurrency?.symbol ?? "tTRUST", + name: chain.nativeCurrency?.name ?? "Native TRUST", decimals: 18, isNative: true, - }, - { - address: addresses.TSWP as Address, - symbol: "TSWP", - name: "TrustSwap", - decimals: 18, - }, - { - address: WNATIVE_ADDRESS, - symbol: "WTTRUST", - name: "Wrapped TRUST", - decimals: 18, - hidden: false, - }, -]; + }); + // Choose wrapped symbol per chain + const wrappedSymbol = chain.id === 1155 ? "WTRUST" : "WTTRUST"; -const TOKEN_CACHE: Record = {}; -for (const t of TOKENLIST) TOKEN_CACHE[t.address.toLowerCase()] = t; + // Wrapped native + if (!isZero(WT)) { + baseTokens.push({ + address: WT, + symbol: wrappedSymbol, + name: "Wrapped TRUST", + decimals: 18, + hidden: false, + }); + } -const client = createPublicClient({ - chain: INTUITION, - transport: http(INTUITION.rpcUrls?.default?.http?.[0] || ""), -}); + // Governance token (optional, may be absent on some networks) + if (!isZero(TSWP_ADDR)) { + baseTokens.push({ + address: TSWP_ADDR, + symbol: "TSWP", + name: "TrustSwap", + decimals: 18, + }); + } -function findToken(addr: string): TokenInfo | null { - return TOKEN_CACHE[addr.toLowerCase()] || null; -} + const TOKENLIST: TokenInfo[] = baseTokens; + const TOKEN_CACHE: Record = {}; + for (const t of TOKENLIST) { + if (!t.address) continue; + TOKEN_CACHE[t.address.toLowerCase()] = t; + } -export const isNative = (addr?: string) => - !!addr && addr.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); + const client = createPublicClient({ + chain, + transport: http(chain.rpcUrls.default.http[0]), + }); -export const toWrapped = (addr: Address): Address => - isNative(addr) ? WNATIVE_ADDRESS : addr; + function findToken(addr: string): TokenInfo | null { + return TOKEN_CACHE[addr.toLowerCase()] || null; + } -export const buildPath = (path: Address[]): Address[] => - path.map(toWrapped) as Address[]; + const isNative = (addr?: string) => + !!addr && addr.toLowerCase() === NATIVE_PLACEHOLDER.toLowerCase(); -export function getTokenByAddress(addr: string | Address): TokenInfo { - const t = TOKENLIST.find( - t => t.address.toLowerCase() === addr.toLowerCase() - ); - if (!t) throw new Error(`Token not in tokenlist: ${addr}`); - return t; -} + const toWrapped = (addr: Address): Address => + isNative(addr) ? WNATIVE_ADDRESS : addr; -export function getDefaultPair(): { tokenIn: TokenInfo; tokenOut: TokenInfo } { - const native = TOKENLIST.find(t => t.isNative) ?? TOKENLIST[0]; - const other = TOKENLIST.find(t => t.address !== native.address && !t.hidden) ?? native; - return { tokenIn: native, tokenOut: other }; -} + const buildPath = (path: Address[]): Address[] => + path.map(toWrapped) as Address[]; -export async function getOrFetchToken(address: Address): Promise { - const cached = findToken(address); - if (cached) return cached; - - const [symbol, decimals, name] = await Promise.all([ - client.readContract({ address, abi: erc20Abi, functionName: "symbol" }).catch(() => "TKN"), - client.readContract({ address, abi: erc20Abi, functionName: "decimals" }).catch(() => 18), - client.readContract({ address, abi: erc20Abi, functionName: "name" }).catch(() => "Unknown"), - ]); - - const info: TokenInfo = { - address, - symbol: String(symbol), - name: String(name), - decimals: Number(decimals), - }; + const isWrapped = (addr?: string) => + !!addr && addr.toLowerCase() === WNATIVE_ADDRESS.toLowerCase(); - TOKEN_CACHE[address.toLowerCase()] = info; - return info; -} + const toUIAddress = (addr?: Address): Address | undefined => + !addr ? undefined : isWrapped(addr) ? NATIVE_PLACEHOLDER : addr; -export const isWrapped = (addr?: string) => - !!addr && addr.toLowerCase() === WNATIVE_ADDRESS.toLowerCase(); + function getTokenByAddress(addr: string | Address): TokenInfo { + const t = TOKENLIST.find( + t => t.address.toLowerCase() === String(addr).toLowerCase(), + ); + if (!t) throw new Error(`Token not in tokenlist: ${addr}`); + return t; + } -/** Adresse à afficher en UI: WTTRUST → tTRUST (placeholder) */ -export const toUIAddress = (addr?: Address): Address | undefined => - !addr ? undefined : isWrapped(addr) ? NATIVE_PLACEHOLDER : addr; + function getDefaultPair(): { tokenIn: TokenInfo; tokenOut: TokenInfo } { + const native = TOKENLIST.find(t => t.isNative) ?? TOKENLIST[0]; + const other = + TOKENLIST.find(t => t.address !== native.address && !t.hidden) ?? native; + return { tokenIn: native, tokenOut: other }; + } -/** Liste de tokens pour l'UI (on masque les hidden = WTTRUST) */ -export function toUIList(list: TokenInfo[]): TokenInfo[] { - return list.filter(t => !t.hidden); -} + async function getOrFetchToken(address: Address): Promise { + const cached = findToken(address); + if (cached) return cached; + + const [symbol, decimals, name] = await Promise.all([ + client + .readContract({ address, abi: erc20Abi, functionName: "symbol" }) + .catch(() => "TKN"), + client + .readContract({ address, abi: erc20Abi, functionName: "decimals" }) + .catch(() => 18), + client + .readContract({ address, abi: erc20Abi, functionName: "name" }) + .catch(() => "Unknown"), + ]); + + const info: TokenInfo = { + address, + symbol: String(symbol), + name: String(name), + decimals: Number(decimals), + }; -/** Récupérer un TokenInfo pour affichage, en tenant compte du mapping UI */ -export function getTokenForUI(addr?: Address): TokenInfo | null { - if (!addr) return null; - const uiAddr = toUIAddress(addr)!; - const t = TOKENLIST.find(x => x.address.toLowerCase() === uiAddr.toLowerCase()); - return t ?? null; -} + TOKEN_CACHE[address.toLowerCase()] = info; + return info; + } + + function toUIList(list: TokenInfo[]): TokenInfo[] { + return list.filter(t => !t.hidden); + } + + function getTokenForUI(addr?: Address): TokenInfo | null { + if (!addr) return null; + const uiAddr = toUIAddress(addr)!; + const t = TOKENLIST.find( + x => x.address.toLowerCase() === uiAddr.toLowerCase(), + ); + return t ?? null; + } + + async function getTokenMetaSafe(addr: Address): Promise { + if (isNative(addr)) { + return { + address: NATIVE_PLACEHOLDER, + symbol: chain.nativeCurrency?.symbol ?? "tTRUST", + name: chain.nativeCurrency?.name ?? "Native TRUST", + decimals: 18, + isNative: true, + }; + } + + const cached = TOKEN_CACHE[addr.toLowerCase()]; + if (cached) return cached; + + return await getOrFetchToken(addr); + } + function getTokenByAddressOrFallback(addr: Address): TokenInfo { + const hit = TOKENLIST.find( + t => t.address.toLowerCase() === addr.toLowerCase(), + ); + if (hit) return hit; -export async function getTokenMetaSafe(addr: Address): Promise { - // natif - if (isNative(addr)) { return { - address: NATIVE_PLACEHOLDER, - symbol: INTUITION?.nativeCurrency?.symbol ?? "tTRUST", - name: INTUITION?.nativeCurrency?.name ?? "Native TRUST", + address: addr, + symbol: "UNK", + name: "Unknown", decimals: 18, - isNative: true, }; } - // cache/local list - const cached = TOKEN_CACHE[addr.toLowerCase()]; - if (cached) return cached; - // on-chain (ne throw pas — a déjà des catchs) - return await getOrFetchToken(addr); -} - -export function getTokenByAddressOrFallback(addr: Address): TokenInfo { - const hit = TOKENLIST.find(t => t.address.toLowerCase() === addr.toLowerCase()); - if (hit) return hit; - - // fallback neutre: ne JAMAIS throw côté UI return { - address: addr, - symbol: "UNK", - name: "Unknown", - decimals: 18, + NATIVE_PLACEHOLDER, + WNATIVE_ADDRESS, + TOKENLIST, + isNative, + isWrapped, + toWrapped, + buildPath, + getTokenByAddress, + getTokenByAddressOrFallback, + getDefaultPair, + getOrFetchToken, + getTokenMetaSafe, + toUIAddress, + toUIList, + getTokenForUI, }; } diff --git a/apps/web/src/lib/wagmi.ts b/apps/web/src/lib/wagmi.ts index 20dea0458..9ccb08121 100644 --- a/apps/web/src/lib/wagmi.ts +++ b/apps/web/src/lib/wagmi.ts @@ -1,22 +1,32 @@ -import { createConfig, http } from "wagmi" -import type { Chain } from "viem" -import { INTUITION } from "@trustswap/sdk" +import { createConfig, http } from "wagmi"; +import type { Chain } from "viem"; +import { intuitionTestnet, intuitionMainnet } from "@trustswap/sdk"; -const MULTICALL3 = import.meta.env.VITE_MULTICALL3 as `0x${string}` | undefined +const MULTICALL3 = import.meta.env.VITE_MULTICALL3 as `0x${string}` | undefined; -const BASE = INTUITION as unknown as Chain -const INTUITION_CHAIN: Chain = { - ...BASE, - contracts: { - ...(BASE.contracts ?? {}), - ...(MULTICALL3 ? { multicall3: { address: MULTICALL3, blockCreated: 0 } } : {}), - }, +function withMulticall(chain: Chain): Chain { + if (!MULTICALL3) return chain; + return { + ...chain, + contracts: { + ...(chain.contracts ?? {}), + multicall3: { + address: MULTICALL3, + blockCreated: 0, + }, + }, + }; } +export const CHAINS: Chain[] = [ + withMulticall(intuitionTestnet as unknown as Chain), + withMulticall(intuitionMainnet as unknown as Chain), +]; + export const wagmiConfig = createConfig({ - chains: [INTUITION_CHAIN], + chains: CHAINS, multiInjectedProviderDiscovery: false, - transports: { - [INTUITION_CHAIN.id]: http(INTUITION_CHAIN.rpcUrls.default.http[0]), - }, -}) + transports: Object.fromEntries( + CHAINS.map((chain) => [chain.id, http(chain.rpcUrls.default.http[0])]), + ), +}); diff --git a/apps/web/src/styles/Layout.module.css b/apps/web/src/styles/Layout.module.css index aafc062da..5a0d49e2e 100644 --- a/apps/web/src/styles/Layout.module.css +++ b/apps/web/src/styles/Layout.module.css @@ -256,4 +256,63 @@ } +.networkSelectContainer { + position: fixed; + top: 3vh; + right: 19vh; + padding: 5px; + z-index: 10000; +} + +.networkSelect { + height: 30px; + border-radius: 2vh; + background-color: var(--black); + color: var(--text-color); + border: 1px solid var(--black-border); + padding: 0 30px 0 16px; /* espace pour la flèche */ + font-size: 11px; + cursor: pointer; + + box-shadow: + inset 0 1px 3px -0.25px rgba(255, 255, 255, 0.12), + inset 0 0.5px 0.25px -0.25px rgba(255, 255, 255, 0.16), + inset 0 -0.75px 0.5px var(--btn-shadow-black), + 0 4px 4px -1px var(--btn-shadow-black), + 0 2px 3px -1px var(--btn-shadow-black), + 0 0.5px 0.5px var(--btn-shadow-black), + 0 0 0 0.75px var(--btn-shadow-black); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + + appearance: none; + outline: none; + transition: all 0.25s ease; + + background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDE0IDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTcgOEwwIDBIMTRMOSAwTDcgOFoiIGZpbGw9IiNmZmYiLz4KPC9zdmc+Cg=="); + background-repeat: no-repeat; + background-position: right 14px center; + background-size: 12px; +} + +/* Hover */ +.networkSelect:hover { + color: var(--text-color); + border-color: rgba(255,255,255,0.18); + transform: translateY(-1px); +} + +/* Active (click) */ +.networkSelect:active { + transform: scale(0.98); +} + +/* Dropdown options */ +.networkSelect option { + background-color: var(--black); + color: var(--text-color); + border: none; + padding: 10px; + font-size: 14px; +} diff --git a/packages/config/src/multicall3.json b/packages/config/src/multicall3.json index e7084584d..d7943f63a 100644 --- a/packages/config/src/multicall3.json +++ b/packages/config/src/multicall3.json @@ -1,4 +1,8 @@ { + "1155": { + "address": "0x31E7C4ef16e1c3c149D2F0a62517d621bDa6D037", + "blockCreated": 117543 + }, "13579": { "address": "0x6E26ea6ab2236a28e3F2B59F532b79273e0Dc575", "blockCreated": 4252441 diff --git a/packages/contracts/deploy/00_deploy_factory.ts b/packages/contracts/deploy/00_deploy_factory.ts index 01fbe4858..e7f6181b3 100644 --- a/packages/contracts/deploy/00_deploy_factory.ts +++ b/packages/contracts/deploy/00_deploy_factory.ts @@ -1,9 +1,26 @@ import { ethers } from "hardhat"; + async function main() { + // Use first signer as temporary feeToSetter (admin for protocol fees) + const [deployer] = await ethers.getSigners(); + const feeToSetter = await deployer.getAddress(); + + const network = await ethers.provider.getNetwork(); + const chainId = Number(network.chainId); + + console.log("Deploying UniswapV2Factory..."); + console.log(" chainId:", chainId); + console.log(" deployer / feeToSetter:", feeToSetter); + const Factory = await ethers.getContractFactory("UniswapV2Factory"); - const feeToSetter = process.env.FEE_TO!; - const f = await Factory.deploy(feeToSetter); - await f.waitForDeployment(); - console.log("Factory:", await f.getAddress()); + const factory = await Factory.deploy(feeToSetter); + await factory.waitForDeployment(); + + const addr = await factory.getAddress(); + console.log("UniswapV2Factory deployed at:", addr); } -main().catch(e=>{console.error(e);process.exit(1)}); + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/packages/contracts/deploy/01_deploy_router.ts b/packages/contracts/deploy/01_deploy_router.ts index f770173aa..a8ddc4d47 100644 --- a/packages/contracts/deploy/01_deploy_router.ts +++ b/packages/contracts/deploy/01_deploy_router.ts @@ -1,9 +1,22 @@ import { ethers } from "hardhat"; + async function main() { const FACTORY = process.env.UNIV2_FACTORY!; const WNATIVE = process.env.WNATIVE!; - const R = await (await ethers.getContractFactory("UniswapV2Router02")).deploy(FACTORY, WNATIVE); - await R.waitForDeployment(); - console.log("Router:", await R.getAddress()); + + console.log("Deploying UniswapV2Router02..."); + console.log(" factory:", FACTORY); + console.log(" WNATIVE:", WNATIVE); + + const Router = await ethers.getContractFactory("UniswapV2Router02"); + const router = await Router.deploy(FACTORY, WNATIVE); + + await router.waitForDeployment(); + + console.log("UniswapV2Router02 deployed at:", await router.getAddress()); } -main().catch(e=>{console.error(e);process.exit(1)}); + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/packages/contracts/deploy/02_check_router.ts b/packages/contracts/deploy/02_check_router.ts new file mode 100644 index 000000000..5b1d47e21 --- /dev/null +++ b/packages/contracts/deploy/02_check_router.ts @@ -0,0 +1,20 @@ +import { ethers } from "hardhat"; + +async function main() { + const routerAddr = "0x5123208Aa3C6A37615327a8c479a5e1654c0200E"; + + const Router = await ethers.getContractFactory("UniswapV2Router02"); + const router = Router.attach(routerAddr); + + const factory = await router.factory(); + const wnative = await router.WETH(); // in UniswapV2, function name is WETH + + console.log("Router:", routerAddr); + console.log(" factory:", factory); + console.log(" WNATIVE:", wnative); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/packages/contracts/deploy/02_set_fee_to.ts b/packages/contracts/deploy/03_set_fee_to.ts similarity index 100% rename from packages/contracts/deploy/02_set_fee_to.ts rename to packages/contracts/deploy/03_set_fee_to.ts diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index ed81d87f2..8dc1dbd54 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -24,6 +24,14 @@ const config: HardhatUserConfig = { .split(",") .map((s) => s.trim()) .filter(Boolean) + }, + intuitionMainnet: { + url: process.env.INTUITION_MAINNET_RPC_URL || "", + chainId: Number(process.env.CHAIN_ID_MAINNET || 0), + accounts: (process.env.PRIVATE_KEY_MAINNET || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean) } } }; diff --git a/packages/sdk/src/addresses/index.ts b/packages/sdk/src/addresses/index.ts index e9c4a12cf..97044e0ad 100644 --- a/packages/sdk/src/addresses/index.ts +++ b/packages/sdk/src/addresses/index.ts @@ -1,4 +1,5 @@ import INTUITION_13579 from "./intuition-testnet.json" assert { type: "json" }; +import INTUITION_MAINNET from "./intuition-mainnet.json" assert { type: "json" }; export type HexAddr = `0x${string}`; @@ -10,14 +11,19 @@ export interface Addresses { NATIVE_PLACEHOLDER: HexAddr; TSWP: HexAddr; WTTRUST: HexAddr; + WTRUST: HexAddr; StakingRewardsFactory: HexAddr; } +export const INTUITION_TESTNET_CHAIN_ID = 13579; +export const INTUITION_MAINNET_CHAIN_ID = 1155; + const BOOK: Record = { - 13579: INTUITION_13579 as Addresses + [INTUITION_TESTNET_CHAIN_ID]: INTUITION_13579 as Addresses, + [INTUITION_MAINNET_CHAIN_ID]: INTUITION_MAINNET as Addresses, }; -export function getAddresses(chainId: number = 13579): Addresses { +export function getAddresses(chainId: number = INTUITION_TESTNET_CHAIN_ID): Addresses { const entry = BOOK[chainId]; if (!entry) { throw new Error(`No addresses for chainId=${chainId}`); @@ -25,5 +31,5 @@ export function getAddresses(chainId: number = 13579): Addresses { return entry; } -// Raccourci par défaut -export const addresses = getAddresses(13579); +// Kept for backward compatibility, but frontend should prefer getAddresses(useChainId()) +export const addresses = getAddresses(INTUITION_TESTNET_CHAIN_ID); diff --git a/packages/sdk/src/addresses/intuition-mainnet.json b/packages/sdk/src/addresses/intuition-mainnet.json new file mode 100644 index 000000000..490fafce8 --- /dev/null +++ b/packages/sdk/src/addresses/intuition-mainnet.json @@ -0,0 +1,10 @@ +{ + "UniswapV2Factory": "0x83E9f4E539eb343F7F67d130a484c8a1b6555458", + "UniswapV2Router02": "0x5123208Aa3C6A37615327a8c479a5e1654c0200E", + "deployer": "0xAd34d98F6D041cb32289F3AF241556a7A627bA09", + "router": "0x5123208Aa3C6A37615327a8c479a5e1654c0200E", + "NATIVE_PLACEHOLDER": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + "WTTRUST": "0x81cFb09cb44f7184Ad934C09F82000701A4bF672", + "TSWP": "0x0000000000000000000000000000000000000000", + "StakingRewardsFactory": "0x0000000000000000000000000000000000000000" +} diff --git a/packages/sdk/src/chains.ts b/packages/sdk/src/chains.ts index bdc855657..9fb463c92 100644 --- a/packages/sdk/src/chains.ts +++ b/packages/sdk/src/chains.ts @@ -1,9 +1,9 @@ // packages/sdk/src/chain.ts import { defineChain } from "viem"; -export const INTUITION = defineChain({ +export const intuitionTestnet = defineChain({ id: 13579, - name: "Intuition Testnet", + name: "Testnet", nativeCurrency: { name: "tTRUST", symbol: "tTRUST", decimals: 18 }, rpcUrls: { default: { http: ["https://testnet.rpc.intuition.systems/http"] }, @@ -19,3 +19,35 @@ export const INTUITION = defineChain({ } } }); + + +export const intuitionMainnet = defineChain({ + id: 1155, + name: "Mainnet", + network: "intuition-mainnet", + nativeCurrency: { + name: "Trust", + symbol: "TRUST", + decimals: 18, + }, + rpcUrls: { + default: { + http: ["https://rpc.intuition.systems/http"], // TODO: mainnet RPC + }, + public: { + http: ["https://rpc.intuition.systems/http"], + }, + }, + blockExplorers: { + default: { + name: "Intuition Mainnet Explorer", + url: "https://explorer.intuition.systems", // TODO: mainnet explorer + }, + }, + contracts: { + multicall3: { + address: "0x31E7C4ef16e1c3c149D2F0a62517d621bDa6D037", + blockCreated: 117543 + } + } +}) \ No newline at end of file