- 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({
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
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}