diff --git a/packages/chains/src/lisk/assets.ts b/packages/chains/src/lisk/assets.ts index 15ff45159a..6b4e97fe26 100644 --- a/packages/chains/src/lisk/assets.ts +++ b/packages/chains/src/lisk/assets.ts @@ -13,6 +13,8 @@ import { wrappedAssetDocs } from "../common"; export const WETH = "0x4200000000000000000000000000000000000006"; export const USDT = "0x05D032ac25d322df992303dCa074EE7392C117b9"; export const LSK = "0xac485391EB2d7D88253a7F1eF18C37f4242D1A24"; +export const USDC = "0xF242275d3a6527d877f2c927a82D9b057609cc71"; +export const WBTC = "0x03C7054BCB39f7b2e5B2c7AcB37583e32D70Cfa3"; export const assets: SupportedAsset[] = [ { @@ -55,6 +57,36 @@ export const assets: SupportedAsset[] = [ initialBorrowCap: parseEther("47000").toString(), initialSupplyCap: parseEther("47000").toString(), initialCf: "0.5" + }, + { + symbol: assetSymbols.USDC, + underlying: USDC, + name: "USD Coin", + decimals: 6, + oracle: OracleTypes.ChainlinkPriceOracleV2, + oracleSpecificParams: { + aggregator: "0xb4e6A7861067674AC398a26DD73A3c524C602184", + feedBaseCurrency: ChainlinkFeedBaseCurrency.USD + } as ChainlinkSpecificParams, + extraDocs: wrappedAssetDocs(SupportedChains.lisk), + initialBorrowCap: parseUnits("25000", 6).toString(), + initialSupplyCap: parseUnits("25000", 6).toString(), + initialCf: "0.5" + }, + { + symbol: assetSymbols.WBTC, + underlying: WBTC, + name: "Wrapped Bitcoin", + decimals: 8, + oracle: OracleTypes.ChainlinkPriceOracleV2, + oracleSpecificParams: { + aggregator: "0x13da43eA89fB692bdB6666F053FeE70aC61A53cd", + feedBaseCurrency: ChainlinkFeedBaseCurrency.USD + } as ChainlinkSpecificParams, + extraDocs: wrappedAssetDocs(SupportedChains.lisk), + initialBorrowCap: parseUnits("0.1", 8).toString(), + initialSupplyCap: parseUnits("0.1", 8).toString(), + initialCf: "0.5" } ]; diff --git a/packages/contracts/tasks/chain-specific/lisk/market.ts b/packages/contracts/tasks/chain-specific/lisk/market.ts index e99b55e1b7..0a3014c58f 100644 --- a/packages/contracts/tasks/chain-specific/lisk/market.ts +++ b/packages/contracts/tasks/chain-specific/lisk/market.ts @@ -5,7 +5,7 @@ import { COMPTROLLER_MAIN } from "."; import { lisk } from "@ionicprotocol/chains"; task("markets:deploy:lisk:new", "deploy new mode assets").setAction(async (_, { viem, run }) => { - const assetsToDeploy: string[] = [assetSymbols.LSK]; + const assetsToDeploy: string[] = [assetSymbols.WBTC]; for (const asset of lisk.assets.filter((asset) => assetsToDeploy.includes(asset.symbol))) { if (!asset.name || !asset.symbol || !asset.underlying) { throw new Error(`Asset ${asset.symbol} has no name, symbol or underlying`); @@ -41,7 +41,7 @@ task("markets:deploy:lisk:new", "deploy new mode assets").setAction(async (_, { }); task("market:set-caps:lisk:new", "Sets CF on a market").setAction(async (_, { viem, run }) => { - const assetsToDeploy: string[] = [assetSymbols.WETH, assetSymbols.USDT, assetSymbols.LSK]; + const assetsToDeploy: string[] = [assetSymbols.USDC, assetSymbols.WBTC]; for (const asset of lisk.assets.filter((asset) => assetsToDeploy.includes(asset.symbol))) { const pool = await viem.getContractAt("IonicComptroller", COMPTROLLER_MAIN); const cToken = await pool.read.cTokensByUnderlying([asset.underlying]); diff --git a/packages/ui/app/_components/MaxDeposit.tsx b/packages/ui/app/_components/MaxDeposit.tsx new file mode 100644 index 0000000000..4f1e067b85 --- /dev/null +++ b/packages/ui/app/_components/MaxDeposit.tsx @@ -0,0 +1,248 @@ +import { + useState, + useMemo, + useRef, + type SetStateAction, + type Dispatch +} from 'react'; + +import dynamic from 'next/dynamic'; +import Image from 'next/image'; + +import { formatUnits, parseUnits, type Address } from 'viem'; +import { useAccount, useBalance, useReadContract } from 'wagmi'; + +import { Button } from '@ui/components/ui/button'; +import { Card, CardContent } from '@ui/components/ui/card'; +import { Input } from '@ui/components/ui/input'; + +import TokenSelector from './stake/TokenSelector'; + +import { icErc20Abi } from '@ionicprotocol/sdk'; + +export interface IBal { + decimals: number; + value: bigint; +} + +interface IMaxDeposit { + amount?: string; + tokenName?: string; + token?: Address; + handleInput?: (val?: string) => void; + fetchOwn?: boolean; + headerText?: string; + max?: string; + chain: number; + tokenSelector?: boolean; + tokenArr?: string[]; + setMaxTokenForUtilization?: Dispatch<SetStateAction<IBal>>; + useUnderlyingBalance?: boolean; + footerText?: string; + decimals?: number; +} + +function MaxDeposit({ + headerText = 'Deposit', + amount, + tokenName = 'eth', + token, + handleInput, + fetchOwn = false, + max, + chain, + tokenSelector = false, + tokenArr, + setMaxTokenForUtilization, + useUnderlyingBalance = false, + footerText, + decimals: propDecimals +}: IMaxDeposit) { + const [bal, setBal] = useState<IBal>(); + const { address } = useAccount(); + + // For regular token balance + const hooktoken = + token === '0x0000000000000000000000000000000000000000' ? undefined : token; + + const { data: regularBalance } = useBalance({ + address, + token: hooktoken, + chainId: chain, + query: { + enabled: !useUnderlyingBalance, + refetchInterval: 5000 + } + }); + + // For underlying token balance + const { data: underlyingBalance } = useReadContract({ + abi: icErc20Abi, + address: token, + functionName: 'balanceOfUnderlying', + args: [address!], + query: { + enabled: useUnderlyingBalance && !!address, + refetchInterval: 5000 + } + }); + + useMemo(() => { + const decimals = propDecimals ?? regularBalance?.decimals ?? 18; + + if (max) { + const value = parseUnits(max, decimals); + setBal({ value, decimals }); + setMaxTokenForUtilization?.({ value, decimals }); + } else if (max === '0') { + setBal({ value: BigInt(0), decimals }); + setMaxTokenForUtilization?.({ value: BigInt(0), decimals }); + } else if (useUnderlyingBalance) { + const value = underlyingBalance ?? BigInt(0); + setBal({ value, decimals }); + setMaxTokenForUtilization?.({ value, decimals }); + } else if (!useUnderlyingBalance && regularBalance) { + setBal({ + value: regularBalance.value, + decimals: regularBalance.decimals + }); + setMaxTokenForUtilization?.({ + value: regularBalance.value, + decimals: regularBalance.decimals + }); + } + }, [ + max, + regularBalance, + underlyingBalance, + useUnderlyingBalance, + propDecimals, + setMaxTokenForUtilization + ]); + + function handlInpData(e: React.ChangeEvent<HTMLInputElement>) { + if ( + bal && + Number(e.target.value) > Number(formatUnits(bal.value, bal.decimals)) + ) + return; + if (!handleInput) return; + handleInput(e.target.value); + } + + function handleMax() { + if (!handleInput || !bal) return; + const maxValue = formatUnits(bal.value, bal.decimals); + handleInput(maxValue); + setMaxTokenForUtilization?.({ + value: bal.value, + decimals: bal.decimals + }); + } + + const newRef = useRef<HTMLDivElement>(null); + const [open, setOpen] = useState<boolean>(false); + + const tokens = tokenName?.split('/') ?? ['eth']; + + return ( + <Card className="border-0 bg-transparent shadow-none"> + <CardContent className="p-0"> + <div + className={`flex w-full mt-2 items-center justify-between text-[11px] text-white/40 ${ + !fetchOwn ? 'flex' : 'hidden' + }`} + > + <span>{headerText}</span> + <div> + {tokenName?.toUpperCase() ?? ''} Balance:{' '} + {bal + ? parseFloat(formatUnits(bal.value, bal.decimals)).toLocaleString( + 'en-US', + { + maximumFractionDigits: 3 + } + ) + : max ?? '0'} + {handleInput && ( + <Button + variant="ghost" + size="xs" + className="text-accent h-4 text-[10px] hover:bg-transparent pr-0" + onClick={handleMax} + > + MAX + </Button> + )} + </div> + </div> + <div className="flex max-w-full items-center gap-x-4"> + <Input + className="focus:outline-none amount-field font-bold bg-transparent disabled:text-white/60 flex-1 min-w-0 border-0 p-0" + placeholder="0.0" + type="number" + value={ + fetchOwn + ? bal && + parseFloat( + formatUnits(bal.value, bal.decimals) + ).toLocaleString('en-US', { + maximumFractionDigits: 3 + }) + : amount + } + onChange={handlInpData} + disabled={!handleInput} + /> + <div className="flex-none flex items-center"> + {tokenSelector ? ( + <TokenSelector + newRef={newRef} + open={open} + setOpen={setOpen} + tokenArr={tokenArr} + selectedToken={tokenName} + /> + ) : ( + <div className="flex items-center"> + <div className="relative flex items-center"> + {tokens.map((token, index) => ( + <div + key={token} + className="relative" + style={{ + marginLeft: index > 0 ? '-0.5rem' : '0', + zIndex: tokens.length - index + }} + > + <Image + src={`/img/symbols/32/color/${token.toLowerCase()}.png`} + alt={`${token} logo`} + width={18} + height={18} + className="rounded-full border border-black bg-black" + onError={(e) => { + const target = e.target as HTMLImageElement; + target.onerror = null; + target.src = '/img/logo/ION.png'; + }} + /> + </div> + ))} + </div> + <span className="ml-2">{tokenName?.toUpperCase()}</span> + </div> + )} + </div> + </div> + {footerText && ( + <div className="flex w-full mt-2 items-center justify-between text-[11px] text-white/40"> + <span>{footerText}</span> + </div> + )} + </CardContent> + </Card> + ); +} + +export default dynamic(() => Promise.resolve(MaxDeposit), { ssr: false }); diff --git a/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx b/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx index f848736dca..728162a92c 100644 --- a/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx +++ b/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx @@ -34,7 +34,6 @@ import SliderComponent from '@ui/app/_components/popup/Slider'; import TransactionStepsHandler, { useTransactionSteps } from '@ui/app/_components/popup/TransactionStepsHandler'; -import type { IBal } from '@ui/app/_components/stake/MaxDeposit'; import { donutoptions, getDonutData @@ -46,8 +45,10 @@ import { useDebounce } from '@ui/hooks/useDebounce'; import { useSupplyCap } from '@ui/hooks/useSupplyCap'; import type { MarketData } from '@ui/types/TokensDataMap'; -import MaxDeposit from './MaxDeposit'; import SwapTo from './SwapTo'; +import MaxDeposit from '../MaxDeposit'; + +import type { IBal } from './SwapTo'; import { collateralSwapAbi } from '@ionicprotocol/sdk/src'; @@ -408,10 +409,8 @@ export default function CollateralSwapPopup({ tokenName={swappedFromAsset.underlyingSymbol.toLowerCase()} token={swappedFromAsset.cToken} handleInput={(val?: string) => setSwapFromAmount(val as string)} - // max="0" chain={+chain} setMaxTokenForUtilization={setMaxTokens} - exchangeRate={swappedFromAsset.exchangeRate} footerText={'$' + (lifiQuote?.estimate?.fromAmountUSD ?? '0')} decimals={swappedFromAsset.underlyingDecimals} /> diff --git a/packages/ui/app/_components/dashboards/MaxDeposit.tsx b/packages/ui/app/_components/dashboards/MaxDeposit.tsx deleted file mode 100644 index 2ae52ec03b..0000000000 --- a/packages/ui/app/_components/dashboards/MaxDeposit.tsx +++ /dev/null @@ -1,206 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; - -import type { Dispatch, SetStateAction } from 'react'; -import { useState, useEffect, useRef } from 'react'; - -import dynamic from 'next/dynamic'; - -import { type Address, formatUnits } from 'viem'; -import { useAccount, useReadContract } from 'wagmi'; - -import TokenSelector from '../stake/TokenSelector'; - -import { icErc20Abi } from '@ionicprotocol/sdk'; - -interface IMaxDeposit { - amount: string; - tokenName: string; - token: Address; - handleInput?: (val?: string) => void; - fetchOwn?: boolean; - headerText?: string; - chain: number; - tokenSelector?: boolean; - tokenArr?: string[]; - setMaxTokenForUtilization?: Dispatch<SetStateAction<IBal>>; - exchangeRate?: bigint; - footerText?: string; - decimals: number; -} - -export interface IBal { - decimals: number; - value: bigint; -} - -function MaxDeposit({ - headerText, - amount, - tokenName, - token, - handleInput, - fetchOwn = false, - tokenSelector = false, - tokenArr, - setMaxTokenForUtilization, - footerText, - decimals -}: IMaxDeposit) { - const [bal, setBal] = useState<IBal>(); - - const { address } = useAccount(); - - const { data } = useReadContract({ - abi: icErc20Abi, - address: token, - functionName: 'balanceOfUnderlying', - args: [address!], - query: { - refetchInterval: 5000 - } - }); - const balance = data ?? 0n; - - // const { data } = useBalance({ - // address, - // token, - // chainId: chain, - // query: { - // refetchInterval: 5000 - // } - // }); - - useEffect(() => { - setMaxTokenForUtilization && - setMaxTokenForUtilization({ - value: balance, - decimals: decimals ?? 18 - }); - data && setBal({ value: balance, decimals: decimals }); - }, [balance, data, decimals, setMaxTokenForUtilization]); - // console.log(data); - function handlInpData(e: React.ChangeEvent<HTMLInputElement>) { - if ( - bal && - Number(e.target.value) > Number(formatUnits(bal.value, bal.decimals)) - ) - return; - if (!handleInput) return; - handleInput(e.target.value); - } - function handleMax(val?: string) { - if (!handleInput || !val) return; - handleInput(val); - } - - const newRef = useRef(null!); - const [open, setOpen] = useState<boolean>(false); - useEffect(() => { - document.addEventListener('mousedown', handleOutsideClick); - return () => { - document.removeEventListener('mousedown', handleOutsideClick); - }; - }, []); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleOutsideClick = (e: any) => { - //@ts-ignore - if (newRef.current && !newRef.current?.contains(e?.target)) { - setOpen(false); - } - }; - return ( - <> - <div - className={`flex w-full mt-2 items-center justify-between text-[11px] text-white/40 ${ - !fetchOwn ? 'flex' : 'hidden' - }`} - > - <span>{headerText}</span> - <div> - {' '} - {tokenName?.toUpperCase() ?? ''} Balance :{' '} - {bal - ? parseFloat(formatUnits(bal?.value, bal?.decimals)).toLocaleString( - 'en-US', - { - maximumFractionDigits: 3 - } - ) - : '0'} - {handleInput && ( - <button - className={`text-accent ml-2`} - onClick={() => { - handleMax(bal ? formatUnits(bal.value, bal.decimals) : '0'); - setMaxTokenForUtilization && - setMaxTokenForUtilization({ - value: bal?.value ?? BigInt(0), - decimals: bal?.decimals ?? 18 - }); - }} - > - MAX - </button> - )} - </div> - </div> - <div - className={`flex max-w-full mt-2 items-center justify-between text-md gap-x-1 `} - > - <input - className={`focus:outline-none amount-field font-bold bg-transparent disabled:text-white/60 flex-auto flex w-full trucnate`} - placeholder={`0.0`} - type="number" - value={ - fetchOwn - ? bal && - parseFloat( - formatUnits(bal?.value, bal?.decimals) - ).toLocaleString('en-US', { - maximumFractionDigits: 3 - }) - : amount - } - onChange={(e) => handlInpData(e)} - disabled={handleInput ? false : true} - /> - <div - className={`ml-auto min-w-max px-0.5 flex items-center justify-end`} - > - {tokenSelector ? ( - <TokenSelector - newRef={newRef} - open={open} - setOpen={setOpen} - // chain={+chain} - tokenArr={tokenArr} - /> - ) : ( - <> - {' '} - <img - alt="ion logo" - className={`w-5 h-5 inline-block ml-2`} - src={`/img/symbols/32/color/${tokenName.toLowerCase()}.png`} - onError={({ currentTarget }) => { - currentTarget.onerror = null; // prevents looping - currentTarget.src = '/img/logo/ION.png'; - }} - /> - <button className={` ml-2`}>{tokenName.toUpperCase()}</button>{' '} - </> - )} - </div> - </div> - <div - className={`flex w-full mt-2 items-center justify-between text-[11px] text-white/40`} - > - <span>{footerText}</span> - </div> - </> - ); -} - -export default dynamic(() => Promise.resolve(MaxDeposit), { ssr: false }); diff --git a/packages/ui/app/_components/markets/StakingTile.tsx b/packages/ui/app/_components/markets/StakingTile.tsx index 4265ca2ca6..beb4d4f039 100644 --- a/packages/ui/app/_components/markets/StakingTile.tsx +++ b/packages/ui/app/_components/markets/StakingTile.tsx @@ -4,8 +4,7 @@ import Link from 'next/link'; import { base, mode } from 'viem/chains'; -import BaseBreakdown from '../stake/BaseBreakdown'; -import ModeBreakdown from '../stake/ModeBreakdown'; +import RewardDisplay from '../stake/RewardDisplay'; interface Iprop { chain: number; @@ -18,22 +17,12 @@ export default function StakingTile({ chain }: Iprop) { > <h1 className={` mr-auto text-xl font-semibold`}>$ION Staking</h1> <div className={`w-full flex flex-col items-center justify-start `}> - {/* <div> - <span>APY</span> - <span>150%</span> - </div> - <div> - <span>Ionic Staked</span> - <span>32678.4</span> - </div> */} - {+chain === mode.id && ( - <ModeBreakdown - step3Toggle="Stake" - selectedToken="eth" + {+chain === mode.id || +chain === base.id ? ( + <RewardDisplay + chainId={+chain} + selectedToken={+chain === mode.id ? 'eth' : undefined} /> - )} - {+chain === base.id && <BaseBreakdown step3Toggle={'Stake'} />} - {+chain !== mode.id && +chain !== base.id && ( + ) : ( <span className="text-sm text-center text-white/50 mx-auto "> Stake your IONs on Base/Mode </span> diff --git a/packages/ui/app/_components/stake/MaxDeposit.tsx b/packages/ui/app/_components/stake/MaxDeposit.tsx deleted file mode 100644 index 75ba829b1c..0000000000 --- a/packages/ui/app/_components/stake/MaxDeposit.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; - -import { - useState, - useMemo, - useEffect, - useRef, - type SetStateAction, - type Dispatch -} from 'react'; - -import dynamic from 'next/dynamic'; - -import { formatUnits, parseUnits } from 'viem'; -// import { mode } from 'viem/chains'; -import { useAccount, useBalance } from 'wagmi'; - -import TokenSelector from './TokenSelector'; - -interface IMaxDeposit { - amount?: string; - tokenName?: string; - token?: `0x${string}`; - handleInput?: (val?: string) => void; - fetchOwn?: boolean; - headerText?: string; - max?: string; - chain: number; - tokenSelector?: boolean; - tokenArr?: string[]; - setMaxTokenForUtilization?: Dispatch<SetStateAction<IBal>>; -} - -export interface IBal { - decimals: number; - value: bigint; -} - -function MaxDeposit({ - headerText = 'Deposit', - amount, - tokenName = 'eth', - token, - handleInput, - fetchOwn = false, - max = '', - chain, - tokenSelector = false, - tokenArr, - setMaxTokenForUtilization -}: IMaxDeposit) { - const [bal, setBal] = useState<IBal>(); - - const { address } = useAccount(); - const hooktoken = - token === '0x0000000000000000000000000000000000000000' ? undefined : token; - - const { data } = useBalance({ - address, - token: hooktoken, - chainId: chain, - query: { - refetchInterval: 5000 - } - }); - - useMemo(() => { - if (max) { - setBal({ - value: parseUnits(max, data?.decimals ?? 18), - decimals: data?.decimals ?? 18 - }); - // setMaxTokenForUtilization && - // setMaxTokenForUtilization({ - // value: parseUnits(max, data?.decimals ?? 18), - // decimals: data?.decimals ?? 18 - // }); - } else if (max == '0') { - setBal({ value: BigInt(0), decimals: data?.decimals ?? 18 }); - // setMaxTokenForUtilization && - // setMaxTokenForUtilization({ - // value: BigInt(0), - // decimals: data?.decimals ?? 18 - // }); - } else { - data && setBal({ value: data?.value, decimals: data?.decimals }); - } - }, [data, max]); - // console.log(data); - function handlInpData(e: React.ChangeEvent<HTMLInputElement>) { - if ( - bal && - Number(e.target.value) > Number(formatUnits(bal?.value, bal?.decimals)) - ) - return; - if (!handleInput) return; - handleInput(e.target.value); - } - function handleMax(val: string) { - if (!handleInput) return; - handleInput(val); - } - - const newRef = useRef(null!); - const [open, setOpen] = useState<boolean>(false); - useEffect(() => { - document.addEventListener('mousedown', handleOutsideClick); - return () => { - document.removeEventListener('mousedown', handleOutsideClick); - }; - }, []); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleOutsideClick = (e: any) => { - //@ts-ignore - if (newRef.current && !newRef.current?.contains(e?.target)) { - setOpen(false); - } - }; - return ( - <> - <div - className={`flex w-full mt-2 items-center justify-between text-[11px] text-white/40 ${ - !fetchOwn ? 'flex' : 'hidden' - }`} - > - <span>{headerText}</span> - <div> - {' '} - {tokenName?.toUpperCase() ?? ''} Balance :{' '} - {bal - ? parseFloat(formatUnits(bal?.value, bal?.decimals)).toLocaleString( - 'en-US', - { - maximumFractionDigits: 3 - } - ) - : max} - {handleInput && ( - <button - className={`text-accent ml-2`} - onClick={() => { - handleMax(bal ? formatUnits(bal?.value, bal?.decimals) : max); - setMaxTokenForUtilization && - setMaxTokenForUtilization({ - value: bal?.value ?? BigInt(0), - decimals: bal?.decimals ?? 18 - }); - }} - > - MAX - </button> - )} - </div> - </div> - <div - className={`flex max-w-full mt-2 items-center justify-between text-md gap-x-1 `} - > - <input - className={`focus:outline-none amount-field font-bold bg-transparent disabled:text-white/60 flex-auto flex w-full trucnate`} - placeholder={`0.0`} - type="number" - value={ - fetchOwn - ? bal && - parseFloat( - formatUnits(bal?.value, bal?.decimals) - ).toLocaleString('en-US', { - maximumFractionDigits: 3 - }) - : amount - } - onChange={(e) => handlInpData(e)} - disabled={handleInput ? false : true} - /> - <div - className={`ml-auto min-w-max px-0.5 flex items-center justify-end`} - > - {tokenSelector ? ( - <TokenSelector - newRef={newRef} - open={open} - setOpen={setOpen} - // chain={+chain} - tokenArr={tokenArr} - /> - ) : ( - <> - {' '} - <img - alt="ion logo" - className={`w-5 h-5 inline-block ml-2`} - src={`/img/symbols/32/color/${tokenName.toLowerCase()}.png`} - onError={({ currentTarget }) => { - currentTarget.onerror = null; // prevents looping - currentTarget.src = '/img/logo/ION.png'; - }} - /> - <button className={` ml-2`}>{tokenName.toUpperCase()}</button>{' '} - </> - )} - </div> - </div> - </> - ); -} - -export default dynamic(() => Promise.resolve(MaxDeposit), { ssr: false }); diff --git a/packages/ui/app/_components/stake/RewardDisplay.tsx b/packages/ui/app/_components/stake/RewardDisplay.tsx new file mode 100644 index 0000000000..cdb3e55f74 --- /dev/null +++ b/packages/ui/app/_components/stake/RewardDisplay.tsx @@ -0,0 +1,114 @@ +import Image from 'next/image'; + +import { base, mode, optimism } from 'viem/chains'; + +import { BaseSugarAddress } from '@ui/constants/baselp'; +import { ModeSugarAddress } from '@ui/constants/lp'; +import { OPSugarAddress } from '@ui/constants/oplp'; +import useSugarAPR from '@ui/hooks/useSugarAPR'; + +type ChainConfig = { + poolIndex: bigint; + sugarAddress: `0x${string}`; + rewardToken: { + name: string; + logo: string; + }; +}; + +type ChainConfigs = { + [key: number]: { + defaultConfig: ChainConfig; + tokenConfigs?: { + [key: string]: ChainConfig; + }; + }; +}; + +export const CHAIN_CONFIGS: ChainConfigs = { + [mode.id]: { + defaultConfig: { + poolIndex: 6n, + sugarAddress: ModeSugarAddress, + rewardToken: { + name: 'Velodrome', + logo: '/img/symbols/32/color/velo.png' + } + }, + tokenConfigs: { + mode: { + poolIndex: 26n, + sugarAddress: ModeSugarAddress, + rewardToken: { + name: 'Velodrome', + logo: '/img/symbols/32/color/velo.png' + } + } + } + }, + [optimism.id]: { + defaultConfig: { + poolIndex: 910n, + sugarAddress: OPSugarAddress, + rewardToken: { + name: 'Velodrome', + logo: '/img/symbols/32/color/velo.png' + } + } + }, + [base.id]: { + defaultConfig: { + poolIndex: 1489n, + sugarAddress: BaseSugarAddress, + rewardToken: { + name: 'Aerodrome', + logo: '/img/logo/AERO.png' + } + } + } +}; + +type RewardDisplayProps = { + chainId: number; + isUnstaking?: boolean; + selectedToken?: 'eth' | 'mode' | 'weth'; +}; + +export default function RewardDisplay({ + chainId, + isUnstaking = false, + selectedToken = 'eth' +}: RewardDisplayProps) { + const chainConfig = CHAIN_CONFIGS[chainId]; + + const config = + chainConfig.tokenConfigs?.[selectedToken] || chainConfig.defaultConfig; + + const { apr } = useSugarAPR({ + sugarAddress: config.sugarAddress, + poolIndex: config.poolIndex, + chainId, + selectedToken, + isMode: chainId === mode.id + }); + + if (!chainConfig) return null; + + return ( + <div className="flex items-center w-full mt-3 text-xs gap-2"> + <div className="w-6 h-6 relative flex-shrink-0"> + <Image + alt={`${config.rewardToken.name} logo`} + src={config.rewardToken.logo} + fill + className="object-contain" + sizes="24px" + /> + </div> + <span>{config.rewardToken.name} APR</span> + <span className={`text-accent ${isUnstaking && 'text-red-500'} ml-auto`}> + {apr} + </span> + </div> + ); +} diff --git a/packages/ui/app/_components/stake/TokenSelector.tsx b/packages/ui/app/_components/stake/TokenSelector.tsx index 2c9227c694..fbb5c0d218 100644 --- a/packages/ui/app/_components/stake/TokenSelector.tsx +++ b/packages/ui/app/_components/stake/TokenSelector.tsx @@ -1,7 +1,7 @@ /* eslint-disable @next/next/no-img-element */ 'use client'; /* eslint-disable @typescript-eslint/no-explicit-any */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; @@ -11,22 +11,19 @@ interface ITokenSelector { open: boolean; setOpen: any; tokenArr?: string[]; - // chain: number; + selectedToken?: string; } export default function TokenSelector({ setOpen, open, newRef, - tokenArr = ['eth', 'weth'] + tokenArr = ['eth', 'weth'], + selectedToken }: ITokenSelector) { const pathname = usePathname(); const searchParams = useSearchParams(); - //URL passed Data ---------------------------- - const queryToken = searchParams.get('token'); - const selectedtokenQuery = queryToken ?? tokenArr[0]; - const createQueryString = useCallback( (value: string) => { const params = new URLSearchParams(searchParams.toString()); @@ -36,65 +33,56 @@ export default function TokenSelector({ [searchParams] ); - const [selectedtoken, setSelectedtoken] = useState<string>(''); - - useEffect(() => { - setSelectedtoken(selectedtokenQuery); - }, [selectedtokenQuery]); return ( <div - className="w-full capitalize text-md relative font-bold" + className="w-full capitalize text-md relative font-bold" ref={newRef} > <div - className={`cursor-pointer my-1 w-full flex flex-col items-start justify-start order border-b-none border-stone-700 `} + className="cursor-pointer my-1 w-full flex flex-col items-start justify-start border-b-none border-stone-700" onClick={() => setOpen((prevState: any) => !prevState)} > <div - className={`py-1.5 pl-3.5 pr-9 text-sm w-full gap-1.5 flex relative items-center justify-start border-2 border-stone-700 ${open ? 'rounded-t-md' : 'rounded-xl '} `} + className={`py-1.5 pl-3.5 pr-9 text-sm w-full gap-1.5 flex relative items-center justify-start border-2 border-stone-700 ${open ? 'rounded-t-md' : 'rounded-xl'}`} > <img alt="symbol" - className={`w-6 inline-block`} - src={`/img/symbols/32/color/${selectedtoken?.toLowerCase()}.png`} + className="w-6 inline-block" + src={`/img/symbols/32/color/${selectedToken?.toLowerCase()}.png`} /> - {selectedtoken?.toUpperCase() ?? 'Select Token'} + {selectedToken?.toUpperCase() ?? 'Select Token'} <img alt="expand-arrow--v2" - className={`w-3 transition-all duration-100 ease-linear absolute right-2 top-1/2 -translate-y-1/2 ${ - open ? 'rotate-180' : 'rotate-0' - } `} - src={`https://img.icons8.com/ios/50/ffffff/expand-arrow--v2.png`} + className={`w-3 transition-all duration-100 ease-linear absolute right-2 top-1/2 -translate-y-1/2 ${open ? 'rotate-180' : 'rotate-0'}`} + src="https://img.icons8.com/ios/50/ffffff/expand-arrow--v2.png" /> </div> - <ul - className={`left-0 ${ - open ? 'block' : 'hidden transition-all delay-1000' - } top-full w-full origin-top z-40 shadow-xl shadow-black/10 rounded-b-md border border-stone-700 absolute bg-grayone/50 backdrop-blur-sm p-1.5 gap-2 max-h-60 overflow-y-auto`} - > - {tokenArr.map((token: string, idx: number) => ( - <Link - className={`flex justify-between items-center p-2 mb-1 text-xs rounded-md`} - href={pathname + '?' + createQueryString(token)} - key={idx} - > - {token.toUpperCase()}{' '} - {selectedtoken === token ? ( - <img - alt="checkmark--v1" - className={`w-4 h-4 stroke-lime`} - src="https://img.icons8.com/ios-filled/50/ffffff/checkmark--v1.png" - /> - ) : ( - <img - alt="logos" - className={`w-4 h-4`} - src={`/img/symbols/32/color/${token.toLowerCase()}.png`} - /> - )} - </Link> - ))} - </ul> + {open && ( + <ul className="left-0 block top-full w-full origin-top z-40 shadow-xl shadow-black/10 rounded-b-md border border-stone-700 absolute bg-grayone/50 backdrop-blur-sm p-1.5 gap-2 max-h-60 overflow-y-auto"> + {tokenArr?.map((token: string, idx: number) => ( + <Link + key={idx} + href={pathname + '?' + createQueryString(token)} + className="flex justify-between items-center p-2 mb-1 text-xs rounded-md" + > + {token.toUpperCase()}{' '} + {selectedToken === token ? ( + <img + alt="checkmark--v1" + className="w-4 h-4 stroke-lime" + src="https://img.icons8.com/ios-filled/50/ffffff/checkmark--v1.png" + /> + ) : ( + <img + alt="logos" + className="w-4 h-4" + src={`/img/symbols/32/color/${token.toLowerCase()}.png`} + /> + )} + </Link> + ))} + </ul> + )} </div> </div> ); diff --git a/packages/ui/app/stake/page.tsx b/packages/ui/app/stake/page.tsx index 12912d783f..9c1ca64281 100644 --- a/packages/ui/app/stake/page.tsx +++ b/packages/ui/app/stake/page.tsx @@ -1,10 +1,10 @@ /* eslint-disable @next/next/no-img-element */ 'use client'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import dynamic from 'next/dynamic'; -import { useSearchParams } from 'next/navigation'; +import { useSearchParams, useRouter } from 'next/navigation'; import { erc20Abi, @@ -38,13 +38,11 @@ import { } from '@ui/utils/getStakingTokens'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; +import MaxDeposit from '../_components/MaxDeposit'; import SliderComponent from '../_components/popup/Slider'; import ResultHandler from '../_components/ResultHandler'; -import BaseBreakdown from '../_components/stake/BaseBreakdown'; import ClaimRewards from '../_components/stake/ClaimRewards'; -import MaxDeposit from '../_components/stake/MaxDeposit'; -import ModeBreakdown from '../_components/stake/ModeBreakdown'; -import OPBreakdown from '../_components/stake/OPBreakdown'; +import RewardDisplay from '../_components/stake/RewardDisplay'; import Toggle from '../_components/Toggle'; const NetworkSelector = dynamic( @@ -67,11 +65,19 @@ export default function Stake() { const [step3Toggle, setstep3Toggle] = useState<string>(''); //--------------- const chainId = useChainId(); + const router = useRouter(); const searchParams = useSearchParams(); const querychain = searchParams.get('chain'); const queryToken = searchParams.get('token'); - const selectedtoken = queryToken ?? 'eth'; const chain = querychain ? querychain : String(chainId); + const previousChain = useRef<string>(); + + const getDefaultToken = (chain: string) => { + return chain === String(mode.id) ? 'mode' : 'eth'; + }; + const selectedtoken = + queryToken ?? getDefaultToken(querychain ?? String(chainId)); + const stakingContractAddress = getStakingToContract( +chain, selectedtoken as 'eth' | 'mode' | 'weth' @@ -81,6 +87,46 @@ export default function Stake() { selectedtoken as 'eth' | 'mode' | 'weth' ); + function resetAllInputs() { + setMaxDeposit({ ion: '', eth: '' }); + setMaxWithdrawl({ ion: '', eth: '' }); + setUtilization(0); + setMaxLp(''); + setMaxUnstake(''); + } + + useEffect(() => { + resetAllInputs(); + }, [step2Toggle, step3Toggle, chain]); + + useEffect(() => { + const params = new URLSearchParams(searchParams); + const currentChain = querychain ?? String(chainId); + let shouldUpdate = false; + + const isChainChange = previousChain.current !== currentChain; + previousChain.current = currentChain; + + const availableTokens = tokenArrOfChain[+currentChain] || ['eth', 'weth']; + const currentToken = params.get('token'); + + if ( + (!currentToken || + (isChainChange && !availableTokens.includes(currentToken))) && + currentChain + ) { + const defaultToken = currentChain === String(mode.id) ? 'mode' : 'eth'; + params.set('token', defaultToken); + shouldUpdate = true; + } + + if (shouldUpdate) { + router.push(`?${params.toString()}`, { scroll: false }); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [chainId, querychain, router, searchParams]); + const { address, isConnected } = useAccount(); const publicClient = usePublicClient(); const { data: walletClient } = useWalletClient(); @@ -612,7 +658,7 @@ export default function Stake() { <MaxDeposit headerText={step2Toggle} amount={maxDeposit.eth} - tokenName={selectedtoken ?? 'eth'} + tokenName={selectedtoken} token={getPoolToken(selectedtoken as 'eth' | 'mode' | 'weth')} chain={+chain} tokenSelector={true} @@ -759,16 +805,15 @@ export default function Stake() { </h1> {/* breakdowns */} - {+chain === mode.id && ( - <ModeBreakdown - step3Toggle={step3Toggle} + {(+chain === mode.id || + +chain === optimism.id || + +chain === base.id) && ( + <RewardDisplay + chainId={+chain} + isUnstaking={step3Toggle === 'Unstake'} selectedToken={selectedtoken as 'eth' | 'mode' | 'weth'} /> )} - {+chain === optimism.id && ( - <OPBreakdown step3Toggle={step3Toggle} /> - )} - {+chain === base.id && <BaseBreakdown step3Toggle={step3Toggle} />} <div className="h-[2px] w-[95%] mx-auto bg-white/10 mt-auto" /> <button disabled={ diff --git a/packages/ui/app/xION/page.tsx b/packages/ui/app/xION/page.tsx index a2cb5e3132..6b6445092e 100644 --- a/packages/ui/app/xION/page.tsx +++ b/packages/ui/app/xION/page.tsx @@ -29,8 +29,8 @@ import { getToken } from '@ui/utils/getStakingTokens'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import { useOutsideClick } from '../../hooks/useOutsideClick'; +import MaxDeposit from '../_components/MaxDeposit'; import ResultHandler from '../_components/ResultHandler'; -import MaxDeposit from '../_components/stake/MaxDeposit'; import FromTOChainSelector from '../_components/xION/FromToChainSelector'; import ProgressSteps from '../_components/xION/ProgressSteps'; import Quote from '../_components/xION/Quote'; diff --git a/packages/ui/components/ui/button.tsx b/packages/ui/components/ui/button.tsx index eeb8151ee4..a7c5815ed0 100644 --- a/packages/ui/components/ui/button.tsx +++ b/packages/ui/components/ui/button.tsx @@ -23,6 +23,7 @@ const buttonVariants = cva( size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', + xs: 'h-8 rounded-md px-2', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10' } diff --git a/packages/ui/constants/BorrowCapDisable.ts b/packages/ui/constants/BorrowCapDisable.ts deleted file mode 100644 index e06f917053..0000000000 --- a/packages/ui/constants/BorrowCapDisable.ts +++ /dev/null @@ -1,12 +0,0 @@ -// multipliers[+dropdownSelectedChain]?.[selectedPoolId]?.[asset] - -import { base, mode } from 'viem/chains'; - -export const disableBorrowRepay: Record<number, Record<string, string[]>> = { - [mode.id]: { - '0': [] - }, - [base.id]: { - '0': [] - } -}; diff --git a/packages/ui/constants/index.ts b/packages/ui/constants/index.ts index b488a7533f..9e4952bbff 100644 --- a/packages/ui/constants/index.ts +++ b/packages/ui/constants/index.ts @@ -308,7 +308,7 @@ export const pools: Record<number, PoolParams> = { { id: '0', name: 'Main Market', - assets: ['WETH', 'USDT', 'LSK'] + assets: ['WETH', 'USDC', 'USDT', 'WBTC', 'LSK'] } ] }