From c4020253d1c212bd7078090d0bc47f516f0a322f Mon Sep 17 00:00:00 2001 From: viet-nv Date: Mon, 12 Aug 2024 16:28:56 +0700 Subject: [PATCH 1/4] feat: add token list --- .../src/components/Content/LiquidityToAdd.tsx | 269 ++++++++++++------ .../src/components/Widget/index.tsx | 39 +-- .../src/hooks/useTokenList.tsx | 58 ++++ .../src/hooks/useZapInState.tsx | 74 ++++- 4 files changed, 331 insertions(+), 109 deletions(-) create mode 100644 packages/liquidity-widgets/src/hooks/useTokenList.tsx diff --git a/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx b/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx index a3c9fc45..4b7092e9 100644 --- a/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx +++ b/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx @@ -2,106 +2,195 @@ import WalletIcon from "../../assets/wallet.svg?react"; import SwitchIcon from "../../assets/switch.svg?react"; import { useZapState } from "../../hooks/useZapInState"; import { formatCurrency, formatWei } from "../../utils"; -import { BigNumber } from "ethers"; -import { formatUnits } from "ethers/lib/utils"; +// import { BigNumber } from "ethers"; +// import { formatUnits } from "ethers/lib/utils"; import { useWidgetInfo } from "../../hooks/useWidgetInfo"; +import { useState } from "react"; +import Modal from "../Modal"; +import { useTokenList } from "../../hooks/useTokenList"; export default function LiquidityToAdd() { - const { amountIn, setAmountIn, tokenIn, toggleTokenIn, balanceIn, zapInfo } = - useZapState(); - const { positionId } = useWidgetInfo(); + const { + // amountIn, + // setAmountIn, + // toggleTokenIn, + balanceIn, + zapInfo, + amountIns, + tokenIns, + onAddNewToken, + onRemoveToken, + onAmountChange, + onTokenInChange, + } = useZapState(); + const { positionId, theme } = useWidgetInfo(); + const { tokens } = useTokenList(); + // TODO const initUsd = zapInfo?.zapDetails.initialAmountUsd; - return ( -
-
- Liquidity to {positionId ? "increase" : "add"} -
-
-
-
- - -
+ const [showTokenModal, setShowTokenModal] = useState(false); -
- - {formatWei(balanceIn, tokenIn?.decimals)} {tokenIn?.symbol} + return ( + <> +
+
+
+ Liquidity to {positionId ? "increase" : "add"}
+
+ {tokenIns.map((tokenIn, index) => { + return ( +
+ {showTokenModal && ( + { + // + }} + > +
Select Token
-
-
- { - const value = e.target.value.replace(/,/g, "."); - const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`); // match escaped "." characters via in a non-capturing group - if ( - value === "" || - inputRegex.test(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) - ) { - setAmountIn(value); - } - }} - inputMode="decimal" - autoComplete="off" - autoCorrect="off" - type="text" - pattern="^[0-9]*[.,]?[0-9]*$" - placeholder="0.0" - minLength={1} - maxLength={79} - spellCheck="false" - /> -
- {!!initUsd && ( -
~{formatCurrency(+initUsd)}
- )} - -
-
+ + +
+ {tokens.map((item) => ( +
{ + setShowTokenModal(false); + onTokenInChange(index, item); + }} + > + {item.symbol} + {item.symbol} +
+ ))} +
+ + )} + +
+
+ + +
-
Zap In with any tokens is coming soon
-
+
+ + {formatWei(balanceIn, tokenIn?.decimals)} {tokenIn?.symbol} + {tokenIns.length > 1 && ( + + )} +
+
+ +
+
+ { + const value = e.target.value.replace(/,/g, "."); + const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`); // match escaped "." characters via in a non-capturing group + if ( + value === "" || + inputRegex.test( + value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ) + ) { + console.log(value) + onAmountChange(index, value); + } + }} + inputMode="decimal" + autoComplete="off" + autoCorrect="off" + type="text" + pattern="^[0-9]*[.,]?[0-9]*$" + placeholder="0.0" + minLength={1} + maxLength={79} + spellCheck="false" + /> +
+ {!!initUsd && ( +
~{formatCurrency(+initUsd)}
+ )} + +
+
+ ); + })} +
+ ); } diff --git a/packages/liquidity-widgets/src/components/Widget/index.tsx b/packages/liquidity-widgets/src/components/Widget/index.tsx index 323d9e2e..ef01bea3 100644 --- a/packages/liquidity-widgets/src/components/Widget/index.tsx +++ b/packages/liquidity-widgets/src/components/Widget/index.tsx @@ -9,6 +9,7 @@ import { NetworkInfo, PoolType } from "../../constants"; import WidgetContent from "../Content"; import { ZapContextProvider } from "../../hooks/useZapInState"; import Setting from "../Setting"; +import { TokenListProvider } from "../../hooks/useTokenList"; export { PoolType }; @@ -70,25 +71,27 @@ export default function Widget({ return ( - - + -
- - -
-
-
+ +
+ + +
+
+ +
); } diff --git a/packages/liquidity-widgets/src/hooks/useTokenList.tsx b/packages/liquidity-widgets/src/hooks/useTokenList.tsx new file mode 100644 index 00000000..9e079736 --- /dev/null +++ b/packages/liquidity-widgets/src/hooks/useTokenList.tsx @@ -0,0 +1,58 @@ +import { + ReactNode, + createContext, + useContext, + useEffect, + useState, +} from "react"; +import { Token } from "../entities/Pool"; +import { useWeb3Provider } from "./useProvider"; + +interface TokenListState { + tokens: Token[]; + loading: boolean; +} + +const TokenListContext = createContext({ + tokens: [], + loading: false, +}); + +export const TokenListProvider = ({ children }: { children: ReactNode }) => { + const [loading, setLoading] = useState(false); + const [tokens, setTokens] = useState([]); + const { chainId } = useWeb3Provider(); + + useEffect(() => { + setLoading(true); + fetch( + `https://ks-setting.kyberswap.com/api/v1/tokens?page=1&pageSize=100&isWhitelisted=true&chainIds=${chainId}` + ) + .then((res) => res.json()) + .then((res) => { + setTokens(res.data.tokens); + }) + .finally(() => { + setLoading(false); + }); + }, [chainId]); + + return ( + + {children} + + ); +}; + +export const useTokenList = () => { + const context = useContext(TokenListContext); + if (context === undefined) { + throw new Error("useWidgetInfo must be used within a WidgetProvider"); + } + return context; +}; diff --git a/packages/liquidity-widgets/src/hooks/useZapInState.tsx b/packages/liquidity-widgets/src/hooks/useZapInState.tsx index 6c876a22..f5e66df3 100644 --- a/packages/liquidity-widgets/src/hooks/useZapInState.tsx +++ b/packages/liquidity-widgets/src/hooks/useZapInState.tsx @@ -144,6 +144,13 @@ const ZapContext = createContext<{ tickUpper: number | null; tokenIn: Token | null; amountIn: string; + tokenIns: Array; + amountIns: string[]; + onAmountChange: (indeX: number, value: string) => void; + onTokenInChange: (indeX: number, value: Token | null) => void; + onAddNewToken: () => void; + onRemoveToken: (index: number) => void; + toggleTokenIn: () => void; balanceIn: string; setAmountIn: (value: string) => void; @@ -173,6 +180,8 @@ const ZapContext = createContext<{ tickLower: null, tickUpper: null, tokenIn: null, + tokenIns: [], + amountIns: [], balanceIn: "0", amountIn: "", toggleTokenIn: () => {}, @@ -197,6 +206,10 @@ const ZapContext = createContext<{ setDegenMode: () => {}, marketPrice: undefined, source: "", + onAmountChange: () => {}, + onTokenInChange: () => {}, + onAddNewToken: () => {}, + onRemoveToken: () => {}, }); export const chainIdToChain: { [chainId: number]: string } = { @@ -277,6 +290,10 @@ export const ZapContextProvider = ({ const [tokenIn, setTokenIn] = useState(null); const [amountIn, setAmountIn] = useState(""); + + const [tokenIns, setTokenIns] = useState>([]); + const [amountIns, setAmountIns] = useState([]); + const [zapInfo, setZapInfo] = useState(null); const [zapApiError, setZapApiError] = useState(""); const [loading, setLoading] = useState(false); @@ -348,14 +365,25 @@ export const ZapContextProvider = ({ } }; + // TODO: deprecated useEffect(() => { if (pool && !tokenIn) setTokenIn(isToken0Native ? nativeToken : pool.token0); }, [pool, tokenIn, nativeToken, isToken0Native]); + useEffect(() => { + if (pool && !tokenIns.length) { + setTokenIns([isToken0Native ? nativeToken : pool.token0]); + setAmountIns([""]); + } + }, [pool, tokenIns.length, nativeToken, isToken0Native]); + const setTick = useCallback( (type: Type, value: number) => { - if (position || (pool && (value > pool.maxTick || value < pool.minTick))) { + if ( + position || + (pool && (value > pool.maxTick || value < pool.minTick)) + ) { return; } @@ -533,6 +561,44 @@ export const ZapContextProvider = ({ source, ]); + const onAmountChange = (index: number, amount: string) => { + if (index >= amountIns.length) { + return; + } + + const newAmountIns = [...amountIns]; + newAmountIns[index] = amount; + setAmountIns(newAmountIns); + }; + const onTokenInChange = (index: number, token: Token | null) => { + if (index >= tokenIns.length) { + return; + } + + const newTokens = [...tokenIns]; + newTokens[index] = token; + setTokenIns(newTokens); + }; + + const onAddNewToken = () => { + setTokenIns((prev) => [...prev, null]); + setAmountIns((prev) => [...prev, ""]); + }; + + const onRemoveToken = (index: number) => { + const newTokenIns = [ + ...tokenIns.slice(0, index), + ...tokenIns.slice(index + 1), + ]; + setTokenIns(newTokenIns); + + const newAmountIns = [ + ...amountIns.slice(0, index), + ...amountIns.slice(index + 1), + ]; + setAmountIns(newAmountIns); + }; + return ( {children} From 93a892cb87a14bc6450faba6fee0b62f3a3ca56f Mon Sep 17 00:00:00 2001 From: viet-nv Date: Mon, 12 Aug 2024 17:54:06 +0700 Subject: [PATCH 2/4] temp --- .../src/components/Content/LiquidityToAdd.tsx | 18 ++-- .../src/hooks/useTokenBalance.ts | 89 ++++++++++++++++++- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx b/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx index 4b7092e9..3c50c1b1 100644 --- a/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx +++ b/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx @@ -8,6 +8,7 @@ import { useWidgetInfo } from "../../hooks/useWidgetInfo"; import { useState } from "react"; import Modal from "../Modal"; import { useTokenList } from "../../hooks/useTokenList"; +import { useTokenBalances } from "../../hooks/useTokenBalance"; export default function LiquidityToAdd() { const { @@ -29,6 +30,9 @@ export default function LiquidityToAdd() { // TODO const initUsd = zapInfo?.zapDetails.initialAmountUsd; + const { balances } = useTokenBalances(tokens.map((item) => item.address)); + console.log(balances) + const [showTokenModal, setShowTokenModal] = useState(false); return ( @@ -38,10 +42,14 @@ export default function LiquidityToAdd() {
Liquidity to {positionId ? "increase" : "add"}
- +
{tokenIns.map((tokenIn, index) => { return ( @@ -156,7 +164,7 @@ export default function LiquidityToAdd() { value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") ) ) { - console.log(value) + console.log(value); onAmountChange(index, value); } }} diff --git a/packages/liquidity-widgets/src/hooks/useTokenBalance.ts b/packages/liquidity-widgets/src/hooks/useTokenBalance.ts index c8cbc95b..c14d5d4b 100644 --- a/packages/liquidity-widgets/src/hooks/useTokenBalance.ts +++ b/packages/liquidity-widgets/src/hooks/useTokenBalance.ts @@ -1,8 +1,11 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import multicallABI from "../abis/multicall.json"; import { useContract } from "./useContract"; import ERC20ABI from "../abis/erc20.json"; import { useWeb3Provider } from "./useProvider"; import { BigNumber } from "ethers"; +import { NATIVE_TOKEN_ADDRESS, NetworkInfo } from "../constants"; +import { Interface } from "ethers/lib/utils"; export default function useTokenBalance(address: string) { const erc20Contract = useContract(address, ERC20ABI, true); @@ -52,3 +55,87 @@ export function useNativeBalance() { return balance; } + +const erc20Interface = new Interface(ERC20ABI); +export const useTokenBalances = (tokenAddresses: string[]) => { + const { provider, chainId, account } = useWeb3Provider(); + const multicallContract = useContract( + NetworkInfo[chainId]?.multiCall, + multicallABI + ); + const [balances, setBalances] = useState<{ [address: string]: BigNumber }>( + {} + ); + const [loading, setLoading] = useState(false); + + const fetchBalances = useCallback(async () => { + if (!provider || !account) { + setBalances({}); + return; + } + try { + setLoading(true); + const nativeBalance = await provider.getBalance(account); + + const fragment = erc20Interface.getFunction("balanceOf"); + const callData = erc20Interface.encodeFunctionData(fragment, [account]); + + const chunks = tokenAddresses + .filter( + (item) => item.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase() + ) + .map((address) => ({ + target: address, + callData, + })); + + const res = await multicallContract?.callStatic.tryBlockAndAggregate( + false, + chunks + ); + const balances = res.returnData.map((item: any) => { + return erc20Interface.decodeFunctionResult(fragment, item.returnData); + }); + setLoading(false); + + setBalances({ + [NATIVE_TOKEN_ADDRESS]: nativeBalance, + ...balances.reduce( + ( + acc: { [address: string]: BigNumber }, + item: { balance: BigNumber }, + index: number + ) => { + return { + ...acc, + [tokenAddresses[index]]: item.balance, + }; + }, + {} as { [address: string]: BigNumber } + ), + }); + } catch (e) { + console.log(e); + setLoading(false); + } + //eslint-disable-next-line react-hooks/exhaustive-deps + }, [provider, account, chainId, JSON.stringify(tokenAddresses)]); + + useEffect(() => { + fetchBalances(); + + const i = setInterval(() => { + fetchBalances(); + }, 10_000); + + return () => { + clearInterval(i); + }; + }, [provider, fetchBalances]); + + return { + loading, + balances, + refetch: fetchBalances, + }; +}; From 4cd1764c320214270db3a8a2a2c2b9d56f945b87 Mon Sep 17 00:00:00 2001 From: viet-nv Date: Thu, 15 Aug 2024 09:53:59 +0700 Subject: [PATCH 3/4] draft ui test for qa --- packages/liquidity-widgets/package.json | 2 +- packages/liquidity-widgets/src/App.tsx | 14 +- .../src/components/Content/LiquidityToAdd.tsx | 149 +++++++++++------- .../src/components/Content/index.tsx | 62 +++++--- .../src/components/Preview/index.tsx | 45 +++--- .../src/hooks/useApproval.ts | 136 +++++++++++++++- .../src/hooks/useTokenBalance.ts | 24 +-- .../src/hooks/useZapInState.tsx | 68 ++++---- 8 files changed, 343 insertions(+), 157 deletions(-) diff --git a/packages/liquidity-widgets/package.json b/packages/liquidity-widgets/package.json index 7864c478..6e033510 100644 --- a/packages/liquidity-widgets/package.json +++ b/packages/liquidity-widgets/package.json @@ -1,7 +1,7 @@ { "name": "@kyberswap/liquidity-widgets", "license": "MIT", - "version": "0.0.8-rc2", + "version": "0.0.8-multi-token-poc-1", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/liquidity-widgets/src/App.tsx b/packages/liquidity-widgets/src/App.tsx index 1a81e71c..d27b8792 100644 --- a/packages/liquidity-widgets/src/App.tsx +++ b/packages/liquidity-widgets/src/App.tsx @@ -42,7 +42,6 @@ init({ label: "Base", rpcUrl: "https://base.llamarpc.com ", }, - ], }); @@ -117,20 +116,13 @@ function App() { boxShadow: "0px 4px 4px rgba(0, 0, 0, 0.04)", }} provider={ethersProvider} - chainId={137} - // positionId="730708" - poolType={PoolType.DEX_UNISWAPV3} - poolAddress="0xb6e57ed85c4c9dbfef2a68711e9d6f36c56e0fcb" - // chainId={56} - // positionId="24654" - // poolType={PoolType.DEX_PANCAKESWAPV3} - // poolAddress="0x36696169c63e42cd08ce11f5deebbcebae652050" - // feeAddress="0x7E59Be2D29C5482256f555D9BD4b37851F1f3411" - // feePcm={50} onDismiss={() => { window.location.reload(); }} source="zap-widget-demo" + chainId={42161} + poolType={PoolType.DEX_UNISWAPV3} + poolAddress="0xC6962004f452bE9203591991D15f6b388e09E8D0" /> ); diff --git a/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx b/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx index 3c50c1b1..8a98d301 100644 --- a/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx +++ b/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx @@ -15,7 +15,7 @@ export default function LiquidityToAdd() { // amountIn, // setAmountIn, // toggleTokenIn, - balanceIn, + // balanceIn, zapInfo, amountIns, tokenIns, @@ -31,9 +31,9 @@ export default function LiquidityToAdd() { const initUsd = zapInfo?.zapDetails.initialAmountUsd; const { balances } = useTokenBalances(tokens.map((item) => item.address)); - console.log(balances) - const [showTokenModal, setShowTokenModal] = useState(false); + const [showTokenModal, setShowTokenModal] = useState(null); + const [search, setSearch] = useState(""); return ( <> @@ -45,7 +45,7 @@ export default function LiquidityToAdd() { )} @@ -182,7 +133,7 @@ export default function LiquidityToAdd() { {!!initUsd && (
~{formatCurrency(+initUsd)}
)} -