diff --git a/packages/liquidity-widgets/package.json b/packages/liquidity-widgets/package.json index 7864c478..84d67143 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-2", "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 a3c9fc45..51ac0e0a 100644 --- a/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx +++ b/packages/liquidity-widgets/src/components/Content/LiquidityToAdd.tsx @@ -1,107 +1,284 @@ 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 { formatCurrency, formatWei, isAddress } from "../../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"; +import { useTokenBalances } from "../../hooks/useTokenBalance"; +import { useTokenFromRpc } from "../../hooks/useTokenFromRpc"; +import { Token } from "../../entities/Pool"; 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, importedTokens, addToken } = useTokenList(); + // TODO const initUsd = zapInfo?.zapDetails.initialAmountUsd; - return ( -
-
- Liquidity to {positionId ? "increase" : "add"} -
-
-
-
- - -
+ const { balances } = useTokenBalances(tokens.map((item) => item.address)); -
- - {formatWei(balanceIn, tokenIn?.decimals)} {tokenIn?.symbol} -
-
+ const [showTokenModal, setShowTokenModal] = useState(null); + const [search, setSearch] = useState(""); + + const filteredTokens = [...tokens, ...importedTokens].filter( + (i) => + i.symbol?.toLowerCase().includes(search.trim().toLowerCase()) || + i.address.toLowerCase() === search.toLowerCase().trim() + ); -
-
- { - 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" - /> + return ( + <> +
+
+
+ Liquidity to {positionId ? "increase" : "add"}
- {!!initUsd && ( -
~{formatCurrency(+initUsd)}
- )} -
+ {tokenIns.map((tokenIn, index) => { + return ( +
+
+
+ + +
+ +
+ + {formatWei( + balances[tokenIn?.address || ""]?.toString() || "0", + 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)}
+ )} + +
+
+ ); + })}
+ {showTokenModal !== null && ( + { + setShowTokenModal(null); + }} + > +
Select Token
-
Zap In with any tokens is coming soon
-
+ setSearch(e.target.value)} + placeholder="Search by token address or symbol" + /> + +
+ {!filteredTokens.length && isAddress(search.trim()) && ( + { + setShowTokenModal(null); + onTokenInChange(showTokenModal, token); + addToken(token); + }} + /> + )} + {filteredTokens + .sort((a, b) => { + const b1 = balances[a.address.toLowerCase()]; + const b2 = balances[b.address.toLowerCase()]; + if (b1 && b2 && b1.gt(b2)) return -1; + return 1; + }) + .map((item) => { + const isSelected = tokenIns + .map((item) => item?.address.toLowerCase()) + .includes(item.address.toLowerCase()); + return ( +
{ + if (isSelected) return; + setShowTokenModal(null); + onTokenInChange(showTokenModal, item); + }} + > +
+ {item.symbol} + {item.symbol} +
+ +
+ {formatWei( + balances[item.address.toLowerCase()]?.toString(), + item.decimals + )} +
+
+ ); + })} +
+ + )} + ); } + +const ImportedToken = ({ + address, + onClick, +}: { + address: string; + onClick: (token: Token) => void; +}) => { + const token = useTokenFromRpc(address); + if (!token) return null; + return ( +
onClick(token)} + > +
+ {token.symbol} + {token.symbol} +
+
+ ); +}; diff --git a/packages/liquidity-widgets/src/components/Content/index.tsx b/packages/liquidity-widgets/src/components/Content/index.tsx index 9fcba6cb..e1953cb1 100644 --- a/packages/liquidity-widgets/src/components/Content/index.tsx +++ b/packages/liquidity-widgets/src/components/Content/index.tsx @@ -14,7 +14,7 @@ import { } from "../../hooks/useZapInState"; import ZapRoute from "./ZapRoute"; import EstLiqValue from "./EstLiqValue"; -import useApproval, { APPROVAL_STATE } from "../../hooks/useApproval"; +import { APPROVAL_STATE, useApprovals } from "../../hooks/useApproval"; import { useEffect, useState } from "react"; import { useWidgetInfo } from "../../hooks/useWidgetInfo"; import Header from "../Header"; @@ -25,6 +25,7 @@ import { PI_LEVEL, formatNumber, getPriceImpact } from "../../utils"; import InfoHelper from "../InfoHelper"; import { BigNumber } from "ethers"; import { useWeb3Provider } from "../../hooks/useProvider"; +import { Token } from "../../entities/Pool"; export default function Content({ onDismiss, @@ -36,9 +37,9 @@ export default function Content({ onTxSubmit?: (tx: string) => void; }) { const { - tokenIn, + tokenIns, zapInfo, - amountIn, + amountIns, error, priceLower, priceUpper, @@ -57,29 +58,38 @@ export default function Content({ const { pool, theme, error: loadPoolError, position } = useWidgetInfo(); const { account } = useWeb3Provider(); - let amountInWei = "0"; + let amountInWeis: string[] = []; try { - amountInWei = parseUnits(amountIn || "0", tokenIn?.decimals).toString(); + amountInWeis = amountIns.map((item, index) => + parseUnits(item || "0", tokenIns[index]?.decimals).toString() + ); } catch { // } - const { loading, approvalState, approve } = useApproval( - amountInWei, - tokenIn?.address || "", + const { loading, approvalStates } = useApprovals( + amountInWeis, + tokenIns.map((item) => item?.address || ""), zapInfo?.routerAddress || "" ); - const [clickedApprove, setClickedLoading] = useState(false); + // const [clickedApprove, _setClickedLoading] = useState(false); const [snapshotState, setSnapshotState] = useState(null); + + // const notApprove = tokenIns.find( + // (item) => + // approvalStates[item?.address || ""] === APPROVAL_STATE.NOT_APPROVED + // ); + const hanldeClick = () => { - if (approvalState === APPROVAL_STATE.NOT_APPROVED) { - setClickedLoading(true); - approve().finally(() => setClickedLoading(false)); - } else if ( + // if (notApprove) { + // setClickedLoading(true); + // approve(notApprove.address).finally(() => setClickedLoading(false)); + // } else + if ( pool && - amountIn && - tokenIn && + // amountIn && + tokenIns.every(Boolean) && zapInfo && priceLower && priceUpper && @@ -90,8 +100,8 @@ export default function Content({ date.setMinutes(date.getMinutes() + (ttl || 20)); setSnapshotState({ - tokenIn, - amountIn, + tokenIns: tokenIns as Token[], + amountIns, pool, zapInfo, priceLower, @@ -116,8 +126,11 @@ export default function Content({ if (error) return error; if (zapLoading) return "Loading..."; if (loading) return "Checking Allowance"; - if (approvalState === APPROVAL_STATE.NOT_APPROVED) return "Approve"; - if (approvalState === APPROVAL_STATE.PENDING) return "Approving"; + + // if (addressToApprove) return "Approving"; + + // if (notApprove) return `Approve ${notApprove.symbol}`; + return "Preview"; })(); @@ -173,11 +186,13 @@ export default function Content({ (!!aggregatorSwapInfo && swapPiRes.level === PI_LEVEL.HIGH); const disabled = - clickedApprove || + // clickedApprove || loading || zapLoading || !!error || - approvalState === APPROVAL_STATE.PENDING || + // Object.values(approvalStates).some( + // (item) => item === APPROVAL_STATE.PENDING + // ) || (piVeryHigh && !degenMode); const newPool = @@ -378,7 +393,10 @@ export default function Content({ disabled={disabled} onClick={hanldeClick} style={ - !disabled && approvalState !== APPROVAL_STATE.NOT_APPROVED + !disabled && + Object.values(approvalStates).some( + (item) => item !== APPROVAL_STATE.NOT_APPROVED + ) ? { background: piVeryHigh && degenMode diff --git a/packages/liquidity-widgets/src/components/Preview/index.tsx b/packages/liquidity-widgets/src/components/Preview/index.tsx index 3f919427..8c31b6a2 100644 --- a/packages/liquidity-widgets/src/components/Preview/index.tsx +++ b/packages/liquidity-widgets/src/components/Preview/index.tsx @@ -41,8 +41,8 @@ import { formatUnits } from "ethers/lib/utils"; export interface ZapState { pool: PoolAdapter; zapInfo: ZapRouteDetail; - tokenIn: Token; - amountIn: string; + tokenIns: Token[]; + amountIns: string[]; priceLower: Price; priceUpper: Price; deadline: number; @@ -71,8 +71,8 @@ export default function Preview({ zapState: { pool, zapInfo, - tokenIn, - amountIn, + tokenIns, + amountIns, priceLower, priceUpper, deadline, @@ -351,13 +351,16 @@ export default function Preview({ {!txHash && (
- Confirm this transaction in your wallet - Zapping{" "} + Confirm this transaction in your wallet + {/* + - Zapping{" "} {formatNumber(+amountIn)} {tokenIn.symbol} into{" "} {positionId ? `Position #${positionId}` : `${getDexName(poolType)} ${pool.token0.symbol}/${ pool.token1.symbol } ${(pool.fee / 10_000) * 100}`} +*/}
)} {txHash && txStatus === "" && ( @@ -488,21 +491,25 @@ export default function Preview({
Zap-in Amount
-
- + {tokenIns.map((tokenIn, index) => { + return ( +
+ -
- {formatNumber(+amountIn)} {tokenIn.symbol}{" "} - - ~{formatCurrency(+zapInfo.zapDetails.initialAmountUsd)} - -
-
+
+ {formatNumber(+amountIns[index])} {tokenIn.symbol}{" "} + + ~{formatCurrency(+zapInfo.zapDetails.initialAmountUsd)} + +
+
+ ); + })}
- - + -
- - -
-
-
+ +
+ + +
+
+ + ); } diff --git a/packages/liquidity-widgets/src/hooks/useApproval.ts b/packages/liquidity-widgets/src/hooks/useApproval.ts index c0da2c5d..6a75bdad 100644 --- a/packages/liquidity-widgets/src/hooks/useApproval.ts +++ b/packages/liquidity-widgets/src/hooks/useApproval.ts @@ -1,9 +1,10 @@ -import { BigNumber, providers } from "ethers"; +import { BigNumber, Contract, providers } from "ethers"; import { useCallback, useEffect, useState } from "react"; import { NATIVE_TOKEN_ADDRESS } from "../constants"; import { useContract } from "./useContract"; import erc20ABI from "../abis/erc20.json"; import { useWeb3Provider } from "./useProvider"; +import { isAddress } from "../utils"; export enum APPROVAL_STATE { UNKNOWN = "unknown", @@ -11,6 +12,10 @@ export enum APPROVAL_STATE { APPROVED = "approved", NOT_APPROVED = "not_approved", } +const MaxUint256: BigNumber = BigNumber.from( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +); + function useApproval( amountToApproveString: string, token: string, @@ -29,9 +34,6 @@ function useApproval( const approve = useCallback(() => { if (contract) { - const MaxUint256: BigNumber = BigNumber.from( - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - ); return contract .approve(spender, MaxUint256) .then((res: providers.TransactionResponse) => { @@ -88,3 +90,129 @@ function useApproval( } export default useApproval; + +export const useApprovals = ( + amounts: string[], + addreses: string[], + spender: string +) => { + const { account, provider } = useWeb3Provider(); + const [loading, setLoading] = useState(false); + const [approvalStates, setApprovalStates] = useState<{ + [address: string]: APPROVAL_STATE; + }>(() => + addreses.reduce((acc, token) => { + return { + ...acc, + [token]: + token === NATIVE_TOKEN_ADDRESS + ? APPROVAL_STATE.APPROVED + : APPROVAL_STATE.UNKNOWN, + }; + }, {}) + ); + const [pendingTx, setPendingTx] = useState(""); + const [addressToApprove, setAddressToApprove] = useState(""); + + const approve = (address: string) => { + const checkedSumAddress = isAddress(address); + if (!checkedSumAddress) return; + setAddressToApprove(address); + const contract = new Contract( + address, + erc20ABI, + provider.getSigner(account) + ); + return contract + .approve(spender, MaxUint256) + .then((res: providers.TransactionResponse) => { + setApprovalStates({ + ...approvalStates, + [address]: APPROVAL_STATE.PENDING, + }); + setPendingTx(res.hash); + }) + .catch(() => { + setAddressToApprove(""); + }); + }; + + useEffect(() => { + if (pendingTx) { + const i = setInterval(() => { + provider?.getTransactionReceipt(pendingTx).then((receipt) => { + if (receipt) { + setPendingTx(""); + setAddressToApprove(""); + setApprovalStates({ + ...approvalStates, + [addressToApprove]: APPROVAL_STATE.APPROVED, + }); + } + }); + }, 8_000); + + return () => { + clearInterval(i); + }; + } + }, [pendingTx, provider, addressToApprove, approvalStates]); + + useEffect(() => { + if (account && spender) { + setLoading(true); + + console.log(1111); + Promise.all( + addreses.map((address, index) => { + if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) + return APPROVAL_STATE.APPROVED; + const contract = new Contract(address, erc20ABI, provider); + const amountToApproveString = amounts[index]; + return contract + .allowance(account, spender) + .then((res: BigNumber) => { + const amountToApprove = BigNumber.from(amountToApproveString); + if (amountToApprove.lte(res)) { + return APPROVAL_STATE.APPROVED; + } else { + return APPROVAL_STATE.NOT_APPROVED; + } + }) + .catch((e: Error) => { + console.log("get allowance failed", e); + return APPROVAL_STATE.UNKNOWN; + }); + }) + ) + .then((res) => { + const tmp = addreses.reduce((acc, address, index) => { + return { + ...acc, + [address]: res[index], + }; + }, {}); + setApprovalStates(tmp); + }) + .finally(() => { + setLoading(false); + }); + } + // eslint-disable-next-line + }, [ + account, + spender, + // eslint-disable-next-line + JSON.stringify(addreses), + // eslint-disable-next-line + JSON.stringify(amounts), + provider, + ]); + + return { + approvalStates, + addressToApprove, + approve, + loading, + }; +}; diff --git a/packages/liquidity-widgets/src/hooks/useTokenBalance.ts b/packages/liquidity-widgets/src/hooks/useTokenBalance.ts index c8cbc95b..0ef3f35f 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,93 @@ 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 addresses = tokenAddresses.filter( + (item) => item.toLowerCase() !== NATIVE_TOKEN_ADDRESS.toLowerCase() + ); + + const chunks = addresses.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, + [NATIVE_TOKEN_ADDRESS.toLowerCase()]: nativeBalance, + ...balances.reduce( + ( + acc: { [address: string]: BigNumber }, + item: { balance: BigNumber }, + index: number + ) => { + if ( + addresses[index].toLowerCase() === + NATIVE_TOKEN_ADDRESS.toLowerCase() + ) + return acc; + return { + ...acc, + [addresses[index].toLowerCase()]: 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, + }; +}; diff --git a/packages/liquidity-widgets/src/hooks/useTokenFromRpc.ts b/packages/liquidity-widgets/src/hooks/useTokenFromRpc.ts new file mode 100644 index 00000000..eaf08dd8 --- /dev/null +++ b/packages/liquidity-widgets/src/hooks/useTokenFromRpc.ts @@ -0,0 +1,35 @@ +import { useContract } from "./useContract"; +import ERC20ABI from "../abis/erc20.json"; +import { useEffect, useState } from "react"; +import { useWeb3Provider } from "./useProvider"; +import { Token } from "../entities/Pool"; + +export const useTokenFromRpc = (address: string) => { + const tokenContract = useContract(address, ERC20ABI); + const { chainId } = useWeb3Provider(); + + const [tokenInfo, setTokenInfo] = useState(null); + + useEffect(() => { + const getInfo = async () => { + const [name, symbol, decimals] = await Promise.all([ + tokenContract?.name(), + tokenContract?.symbol(), + tokenContract?.decimals(), + ]); + + setTokenInfo({ + address, + name, + symbol, + decimals, + chainId, + logoURI: `https://ui-avatars.com/api/?background=0D8ABC&color=fff&name=${name}`, + }); + }; + + getInfo(); + }, [tokenContract, address, chainId]); + + return tokenInfo; +}; diff --git a/packages/liquidity-widgets/src/hooks/useTokenList.tsx b/packages/liquidity-widgets/src/hooks/useTokenList.tsx new file mode 100644 index 00000000..3e3f8979 --- /dev/null +++ b/packages/liquidity-widgets/src/hooks/useTokenList.tsx @@ -0,0 +1,110 @@ +import { + ReactNode, + createContext, + useContext, + useEffect, + useState, +} from "react"; +import { Token } from "../entities/Pool"; +import { useWeb3Provider } from "./useProvider"; + +interface TokenListState { + tokens: Token[]; + loading: boolean; + + importedTokens: Token[]; + addToken: (token: Token) => void; + removeToken: (token: Token) => void; +} + +const TokenListContext = createContext({ + tokens: [], + loading: false, + importedTokens: [], + addToken: () => { + // + }, + removeToken: () => { + // + }, +}); + +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]); + + const [importedTokens, setImportedTokens] = useState(() => { + if (typeof window !== "undefined") { + try { + const localStorageTokens = JSON.parse( + localStorage.getItem("importedTokens") || "[]" + ); + + return localStorageTokens; + } catch (e) { + return []; + } + } + + return []; + }); + + const addToken = (token: Token) => { + const newTokens = [ + ...importedTokens.filter((t) => t.address !== token.address), + token, + ]; + setImportedTokens(newTokens); + if (typeof window !== "undefined") + localStorage.setItem("importedTokens", JSON.stringify(newTokens)); + }; + + const removeToken = (token: Token) => { + const newTokens = importedTokens.filter( + (t) => + t.address.toLowerCase() !== token.address.toLowerCase() && + t.chainId === token.chainId + ); + + setImportedTokens(newTokens); + if (typeof window !== "undefined") + localStorage.setItem("importedTokens", JSON.stringify(newTokens)); + }; + + 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..1ac99a21 100644 --- a/packages/liquidity-widgets/src/hooks/useZapInState.tsx +++ b/packages/liquidity-widgets/src/hooks/useZapInState.tsx @@ -13,11 +13,11 @@ import { parseUnits } from "ethers/lib/utils"; import useTokenBalance, { useNativeBalance } from "./useTokenBalance"; import { Price, tickToPrice, Token } from "../entities/Pool"; import { NATIVE_TOKEN_ADDRESS, NetworkInfo } from "../constants"; -import { BigNumber } from "ethers"; +// import { BigNumber } from "ethers"; import useDebounce from "./useDebounce"; -export const ZAP_URL = "https://zap-api.kyberswap.com"; -// export const ZAP_URL = "https://pre-zap-api.kyberengineering.io"; +// export const ZAP_URL = "https://zap-api.kyberswap.com"; +export const ZAP_URL = "https://pre-zap-api.kyberengineering.io"; export interface AddLiquidityAction { type: "ACTION_TYPE_ADD_LIQUIDITY"; @@ -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); @@ -284,7 +301,7 @@ export const ZapContextProvider = ({ const debounceTickLower = useDebounce(tickLower, 300); const debounceTickUpper = useDebounce(tickUpper, 300); - const debounceAmountIn = useDebounce(amountIn, 300); + const debounceAmountIns = useDebounce(amountIns, 300); const toggleRevertPrice = useCallback(() => { setRevertPrice((prev) => !prev); @@ -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; } @@ -384,31 +412,31 @@ export const ZapContextProvider = ({ if (!account) return "Please connect wallet"; if (chainId !== networkChainId) return "Wrong network"; - if (!tokenIn) return "Select token in"; + if (!tokenIns.length) return "Select token in"; if (tickLower === null) return "Enter min price"; if (tickUpper === null) return "Enter max price"; if (tickLower >= tickUpper) return "Invalid price range"; - if (!amountIn || +amountIn === 0) return "Enter an amount"; - try { - const amountInWei = parseUnits(amountIn, tokenIn.decimals); - if (amountInWei.gt(BigNumber.from(balanceIn))) - return "Insufficient balance"; - } catch (e) { - return "Invalid input amount"; - } + // if (!amountIn || +amountIn === 0) return "Enter an amount"; + // try { + // const amountInWei = parseUnits(amountIn, tokenIn.decimals); + // if (amountInWei.gt(BigNumber.from(balanceIn))) + // return "Insufficient balance"; + // } catch (e) { + // return "Invalid input amount"; + // } if (zapApiError) return zapApiError; return ""; }, [ - tokenIn, + tokenIns.length, tickLower, tickUpper, - amountIn, + // amountIn, account, zapApiError, - balanceIn, + // balanceIn, networkChainId, chainId, ]); @@ -444,18 +472,24 @@ export const ZapContextProvider = ({ if ( debounceTickLower !== null && debounceTickUpper !== null && - debounceAmountIn && + debounceAmountIns && pool && - tokenIn?.address && - +debounceAmountIn !== 0 + +debounceAmountIns !== 0 ) { - let amountInWei = ""; + let amountInWeis: (string | null)[] = []; try { - amountInWei = parseUnits(debounceAmountIn, tokenIn.decimals).toString(); + amountInWeis = debounceAmountIns.map((item, index) => { + if (tokenIns[index]) + return parseUnits( + item, + (tokenIns[index] as Token).decimals + ).toString(); + return null; + }); } catch (error) { console.log(error); } - if (!amountInWei) { + if (!amountInWeis.length || !amountInWeis.every(Boolean)) { return; } @@ -463,13 +497,12 @@ export const ZapContextProvider = ({ const params: { [key: string]: string | number | boolean } = { dex: poolType, "pool.id": poolAddress, - "pool.token0": pool.token0.address, - "pool.token1": pool.token1.address, - "pool.fee": pool.fee, + "pool.tokens": `${pool.token0.address},${pool.token1.address}`, + // "pool.fee": pool.fee, "position.tickUpper": debounceTickUpper, "position.tickLower": debounceTickLower, - tokenIn: tokenIn.address, - amountIn: amountInWei, + tokensIn: tokenIns.map((item) => item?.address).join(","), + amountsIn: amountInWeis.join(), slippage, "aggregatorOptions.disable": !enableAggregator, ...(positionId ? { "position.id": positionId } : {}), @@ -514,17 +547,16 @@ export const ZapContextProvider = ({ }); } }, [ - debounceAmountIn, + debounceAmountIns, chainId, poolType, debounceTickLower, debounceTickUpper, feeAddress, feePcm, - tokenIn?.address, poolAddress, pool, - tokenIn?.decimals, + tokenIns, enableAggregator, slippage, positionId, @@ -533,6 +565,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}