const { // defaultPair, // pair, data, toast, prices, addresses, defaultDex, proxyAddress, addAction, userPositions, VAULT_ADDRESS, BALANCER_QUERIES, ICON_VAULT_MAP } = props; const { Row, Column, DetailWrapper, FilterButtonList, FilterButton, InputWrapList, InputWrap, Input, InputSuffix, StyledImageList, PriceWrap, TotalPrice, BalancePrice, StyledButtonList, StyledButton, } = VM.require('bluebiu.near/widget/Liquidity.Handler.Styles') const defaultDeposit = props.tab === "deposit" || !props.tab; const curPositionUSD = userPositions[data.vaultAddress]?.balanceUSD; const APPROVE_SENDER_ADDRESS = "0xBA12222222228d8Ba445958a75a0704d566BF2C8" const VAULT_ADDRESS_ABI = [{ "inputs": [ { "internalType": "bytes32", "name": "poolId", "type": "bytes32" } ], "name": "getPoolTokens", "outputs": [ { "internalType": "contract IERC20[]", "name": "tokens", "type": "address[]" }, { "internalType": "uint256[]", "name": "balances", "type": "uint256[]" }, { "internalType": "uint256", "name": "lastChangeBlock", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "bytes32", "name": "poolId", "type": "bytes32" }, { "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "components": [ { "internalType": "contract IAsset[]", "name": "assets", "type": "address[]" }, { "internalType": "uint256[]", "name": "maxAmountsIn", "type": "uint256[]" }, { "internalType": "bytes", "name": "userData", "type": "bytes" }, { "internalType": "bool", "name": "fromInternalBalance", "type": "bool" } ], "internalType": "struct IVault.JoinPoolRequest", "name": "request", "type": "tuple" } ], "name": "joinPool", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [ { "internalType": "bytes32", "name": "poolId", "type": "bytes32" }, { "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "address payable", "name": "recipient", "type": "address" }, { "components": [ { "internalType": "contract IAsset[]", "name": "assets", "type": "address[]" }, { "internalType": "uint256[]", "name": "minAmountsOut", "type": "uint256[]" }, { "internalType": "bytes", "name": "userData", "type": "bytes" }, { "internalType": "bool", "name": "toInternalBalance", "type": "bool" } ], "internalType": "struct IVault.ExitPoolRequest", "name": "request", "type": "tuple" } ], "name": "exitPool", "outputs": [], "stateMutability": "nonpayable", "type": "function" }] const BALANCER_QUERIES_ABI = [{ "inputs": [ { "internalType": "bytes32", "name": "poolId", "type": "bytes32" }, { "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "components": [ { "internalType": "contract IAsset[]", "name": "assets", "type": "address[]" }, { "internalType": "uint256[]", "name": "maxAmountsIn", "type": "uint256[]" }, { "internalType": "bytes", "name": "userData", "type": "bytes" }, { "internalType": "bool", "name": "fromInternalBalance", "type": "bool" } ], "internalType": "struct IVault.JoinPoolRequest", "name": "request", "type": "tuple" } ], "name": "queryJoin", "outputs": [ { "internalType": "uint256", "name": "bptOut", "type": "uint256" }, { "internalType": "uint256[]", "name": "amountsIn", "type": "uint256[]" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "bytes32", "name": "poolId", "type": "bytes32" }, { "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "components": [ { "internalType": "contract IAsset[]", "name": "assets", "type": "address[]" }, { "internalType": "uint256[]", "name": "minAmountsOut", "type": "uint256[]" }, { "internalType": "bytes", "name": "userData", "type": "bytes" }, { "internalType": "bool", "name": "toInternalBalance", "type": "bool" } ], "internalType": "struct IVault.ExitPoolRequest", "name": "request", "type": "tuple" } ], "name": "queryExit", "outputs": [ { "internalType": "uint256", "name": "bptIn", "type": "uint256" }, { "internalType": "uint256[]", "name": "amountsOut", "type": "uint256[]" } ], "stateMutability": "nonpayable", "type": "function" }] const BPT_ADDRESS_ABI = [{ "inputs": [], "name": "totalSupply", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "account", "type": "address" } ], "name": "balanceOf", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }] State.init({ isDeposit: defaultDeposit, balances: { }, exitBalances: { }, amount0: "", amount1: "", exitAmount0: "", exitAmount1: "", isError: false, isLoading: false, isToken0Approved: true, isToken1Approved: true, isToken0Approving: false, isToken1Approving: false, loadingMsg: "", isPostTx: false, showPairs: false, }); const sourceBalances = { } const { isDeposit, balances, amount0, amount1, exitAmount0, exitAmount1, isLoading, isError, isToken0Approved, isToken1Approved, isToken0Approving, isToken1Approving, loadingMsg, exitBalances, isPostTx, } = state; const detailLoading = Object.keys(balances).length < 2 const sender = Ethers.send("eth_requestAccounts", [])[0]; const { token0, token1, decimals0, decimals1, id } = data || defaultPair; const vaultAddress = addresses[id]; const tokensPrice = prices; const isInSufficient = Number(amount0) > Number(balances[token0]) || Number(amount1) > Number(balances[token1]); const isWithdrawInsufficient = Number(exitAmount0) > Number(exitBalances[token0]) || Number(exitAmount1) > Number(exitBalances[token1]) const balance0 = !amount0 || !tokensPrice?.[token0] ? "-" : parseFloat(Big(amount0).times(tokensPrice[token0]).toFixed(4)); const balance1 = !amount1 || !tokensPrice?.[token1] ? "-" : parseFloat(Big(amount1).times(tokensPrice[token1]).toFixed(4)); const getFromDepositAmount = (depositAmount, tokenDecimal) => { let a = new Big(depositAmount[0].toString()); let b = new Big(depositAmount[1].toString()); if (a.eq(0) && b.eq(0)) return "0"; let diff; let midpoint; if (a.gt(b)) { diff = a.minus(b); midpoint = diff.div(new Big(2)).plus(b); } else { diff = b.minus(a); midpoint = diff.div(new Big(2)).plus(a); } for (let i = tokenDecimal; i > 0; i--) { const midpointFixed = midpoint .div(new Big(10).pow(tokenDecimal)) .toFixed(i); if ( a.div(Big(10).pow(tokenDecimal)).lte(midpointFixed) && b.div(Big(10).pow(tokenDecimal)).gte(midpointFixed) ) { return midpointFixed; } } return "0"; }; const updateBalance = (token) => { const { address, decimals, symbol } = token; if (symbol === "ETH") { Ethers.provider() .getBalance(sender) .then((balanceBig) => { const adjustedBalance = ethers.utils.formatEther(balanceBig); sourceBalances[symbol] = adjustedBalance State.update({ balances: sourceBalances, }); }); } else { const erc20Abi = ["function balanceOf(address) view returns (uint256)"]; const tokenContract = new ethers.Contract( address, erc20Abi, Ethers.provider() ); tokenContract.balanceOf(sender) .then((balanceBig) => { const adjustedBalance = Big( ethers.utils.formatUnits(balanceBig, decimals) ).toFixed(); sourceBalances[symbol] = adjustedBalance State.update({ balances: sourceBalances, }); }) .catch(error => { console.log('error: ', error); setTimeout(() => { updateBalance(token) }, 1500) }); } }; const updateExitBalance = () => { handleGetPoolTokens(result => { const [_tokens, _balances] = result const bptAddress = addresses[data.id] handleGetTotalSupply( bptAddress, totalSupply => { const contract = new ethers.Contract( bptAddress, BPT_ADDRESS_ABI, Ethers.provider() ); contract .balanceOf(sender) .then(balance => { const yourPoolShare = Big(ethers.utils.formatUnits(balance, 18)).div(totalSupply) State.update({ exitBalances: { [token0]: Big(ethers.utils.formatUnits(_balances[0], decimals0)).times(yourPoolShare).toFixed(), [token1]: Big(ethers.utils.formatUnits(_balances[1], decimals1)).times(yourPoolShare).toFixed() } }) }) } ) }) } const handleCheckApproval = (symbol, amount, decimals) => { const wei = ethers.utils.parseUnits( Big(amount).toFixed(decimals), decimals ); const abi = [ "function allowance(address, address) external view returns (uint256)", ]; const contract = new ethers.Contract( addresses[symbol], abi, Ethers.provider() ); return new Promise((resolve) => { contract .allowance(sender, vaultAddress) .then((allowance) => { const approved = !new Big(allowance.toString()).lt(wei) console.log('=allowance', allowance) console.log('=approved', approved) State.update({ [symbol === token0 ? 'isToken0Approved' : 'isToken1Approved']: approved, }); resolve(approved) }) .catch((e) => console.log(e)); }) } const checkApproval = (amount, otherAmount, symbol, poolTokens, callback) => { const otherSymbol = symbol === token0 ? token1 : token0 const decimals = symbol === token0 ? decimals0 : decimals1 const otherDecimals = symbol === token0 ? decimals1 : decimals0 const promiseArray = [ handleCheckApproval(symbol, amount, decimals), handleCheckApproval(otherSymbol, otherAmount, otherDecimals) ] Promise.all(promiseArray).then(result => { const [firstApproved, secondApproved] = result if (callback) { if (firstApproved && secondApproved) { symbol === token0 ? callback(amount, otherAmount, poolTokens) : callback(otherAmount, amount, poolTokens) } else { toast?.dismiss(state.toastId); State.update({ isLoading: false }) } } }) }; const changeMode = (isDeposit) => { State.update({ isDeposit }); }; const handleMax = (isToken0) => { if (isToken0) handleTokenChange(balances[token0], token0); else handleTokenChange(balances[token1], token1); }; const handleGetPoolTokens = (callback) => { const contract = new ethers.Contract( VAULT_ADDRESS, VAULT_ADDRESS_ABI, Ethers.provider().getSigner() ); contract .getPoolTokens(data?.poolId) .then(result => { callback && callback(result) }) } const handleTokenChange = (amount, symbol, callback) => { if (Number(amount) < 0) { return } if (Number(amount) === 0) { State.update({ [symbol === token0 ? 'amount0' : 'amount1']: amount, isToken0Approved: true, isToken1Approved: true, }); return; } State.update({ [symbol === token0 ? 'amount0' : 'amount1']: amount, isLoading: true, isError: false, loadingMsg: "Computing deposit amount...", }); handleGetPoolTokens(poolTokens => { const _balance0 = ethers.utils.formatUnits(poolTokens[1][0], decimals0) const _balance1 = ethers.utils.formatUnits(poolTokens[1][1], decimals1) const balanceRatio = Big(_balance1).div(_balance0).toFixed() const otherAmount = symbol === token0 ? Big(amount).times(balanceRatio).toFixed(6) : Big(amount).div(balanceRatio).toFixed(6) State.update({ [symbol === token0 ? 'amount1' : 'amount0']: otherAmount, focusedSymbol: symbol, isLoading: callback ? true : false }); checkApproval(amount, otherAmount, symbol, poolTokens, callback); }) }; const handleExitAmountChange = (amount, symbol) => { State.update({ [symbol === token0 ? 'exitAmount0' : 'exitAmount1']: amount, }) } const handleApprove = (isToken0) => { const _token = isToken0 ? token0 : token1; const payload = isToken0 ? { isToken0Approving: true } : { isToken1Approving: true }; const amount = isToken0 ? Big(amount0).toFixed(decimals0) : Big(amount1).toFixed(decimals1); const toastId = toast?.loading({ title: `Approve ${_token}`, }); State.update({ ...payload, isLoading: true, loadingMsg: `Approving ${_token}...`, }); const tokenWei = ethers.utils.parseUnits( amount, isToken0 ? decimals0 : decimals1 ); const abi = ["function approve(address, uint) public"]; const tokenContract = new ethers.Contract( addresses[_token], abi, Ethers.provider().getSigner() ); tokenContract .approve(vaultAddress, tokenWei) .then((tx) => tx.wait()) .then((receipt) => { const payload = isToken0 ? { isToken0Approved: true, isToken0Approving: false } : { isToken1Approved: true, isToken1Approving: false }; State.update({ ...payload, isLoading: false, loadingMsg: "" }); toast?.dismiss(toastId); toast?.success({ title: "Approve Successfully!", // text: `Approve ${amount} ${_token}`, tx: receipt.transactionHash, chainId: props.chainId, }); }) .catch((error) => { console.log('error: ', error) State.update({ isError: true, isLoading: false, loadingMsg: error, isToken0Approving: false, isToken1Approving: false, }); toast?.dismiss(toastId); toast?.fail({ title: "Approve Failed!", text: error?.message?.includes("user rejected transaction") ? "User rejected transaction" : null }); }); }; const handleGetUserData = (joinKind, initBalances) => { const abiCoder = new ethers.utils.AbiCoder(); const userDataEncoded = abiCoder.encode( ["uint256", "uint256"], [joinKind, initBalances] ); return userDataEncoded } const handleGetTotalSupply = (bptAddress, callback) => { const contract = new ethers.Contract( bptAddress, BPT_ADDRESS_ABI, Ethers.provider().getSigner() ); contract .totalSupply() .then(result => { callback && callback(ethers.utils.formatUnits(result, 18)) }) } const handleGetBptOutAndAmountsIn = (amount, otherAmount, poolTokens, type, callback) => { handleGetTotalSupply(addresses[data.id], totalSupply => { const [_tokens, _balances] = poolTokens const _balance0 = ethers.utils.formatUnits(_balances[0], decimals0) const _balance1 = ethers.utils.formatUnits(_balances[1], decimals1) const bptPriceUsd = Big(Big(_balance0).times(prices[token0]).plus(Big(_balance1).times(prices[token1]))).div(totalSupply) const bptValue = Big(Big(amount).times(prices[token0]).plus(Big(otherAmount).times(prices[token1]))) const initBalances = ethers.BigNumber.from(ethers.utils.parseUnits(Big(bptValue).div(bptPriceUsd).toFixed(18), 18)) const userData = handleGetUserData(type === 'deposit' ? 3 : 1, initBalances) const contract = new ethers.Contract( BALANCER_QUERIES, BALANCER_QUERIES_ABI, Ethers.provider().getSigner() ); const wei = ethers.utils.parseUnits( Big(amount).toFixed(decimals0), decimals0 ) const otherWei = ethers.utils.parseUnits( Big(otherAmount).toFixed(decimals1), decimals1 ) const callStaticMethod = type === 'deposit' ? contract.callStatic.queryJoin : contract.callStatic.queryExit callStaticMethod( data.poolId, sender, sender, [_tokens, [wei, otherWei], userData, false] ).then(result => { callback && callback(result) }) }) } const handleDeposit = () => { const toastId = toast?.loading({ title: `Depositing...`, }); State.update({ toastId, isLoading: true, isError: false, loadingMsg: "Depositing...", }); handleTokenChange( state.focusedSymbol === token0 ? amount0 : amount1, state.focusedSymbol === token0 ? token0 : token1, (amount, otherAmount, poolTokens) => { handleGetBptOutAndAmountsIn(amount, otherAmount, poolTokens, 'deposit', result => { const [_tokens] = poolTokens const [bptOut, amountsIn] = result const contract = new ethers.Contract( VAULT_ADDRESS, VAULT_ADDRESS_ABI, Ethers.provider().getSigner() ); contract .joinPool( data.poolId, sender, sender, [_tokens, amountsIn, handleGetUserData(3, bptOut), false] ) .then(tx => tx.wait()) .then((receipt) => { const { status, transactionHash } = receipt; addAction?.({ type: "Liquidity", action: "Deposit", token0, token1, amount: amount0, template: defaultDex, status: status, add: 1, transactionHash, chain_id: props.chainId, extra_data: JSON.stringify({ action: "Deposit", amount0, amount1, }) }); State.update({ isLoading: false, isPostTx: true, }); setTimeout(() => State.update({ isPostTx: false }), 10_000); const { refetch } = props; if (refetch) { refetch() } toast?.dismiss(toastId); toast?.success({ title: "Deposit Successfully!", }); }) .catch((error) => { console.log('error: ', error) State.update({ isError: true, isLoading: false, loadingMsg: error, }); toast?.dismiss(toastId); toast?.fail({ title: "Deposit Failed!", text: error?.message?.includes("user rejected transaction") ? "User rejected transaction" : error?.message ?? "", }); }); }) }) }; const handleWithdraw = () => { const toastId = toast?.loading({ title: `Withdrawing...`, }); State.update({ isLoading: true, isError: false, loadingMsg: "Withdrawing...", }); handleGetPoolTokens(poolTokens => { handleGetBptOutAndAmountsIn(exitAmount0, exitAmount1, poolTokens, 'withdraw', result => { const [_tokens] = poolTokens const [bptOut, amountsIn] = result const contract = new ethers.Contract( VAULT_ADDRESS, VAULT_ADDRESS_ABI, Ethers.provider().getSigner() ); contract .exitPool( data.poolId, sender, sender, [_tokens, amountsIn, handleGetUserData(1, bptOut), false] ) .then(tx => tx.wait()) .then((receipt) => { State.update({ isLoading: false, isPostTx: true, }); const { status, transactionHash } = receipt; addAction?.({ type: "Liquidity", action: "Withdraw", token0, token1, amount: lpAmount, template: defaultDex, status: status, add: 0, transactionHash, chain_id: state.chainId, // extra_data: JSON.stringify({ // action: "Withdraw", // amount0: ethers.utils.formatUnits(result[0], decimals0), // amount1: ethers.utils.formatUnits(result[1], decimals1), // }) }); setTimeout(() => State.update({ isPostTx: false }), 10_000); const { refetch } = props; if (refetch) { setTimeout(() => { refetch(); }, 3000) } toast?.dismiss(toastId); toast?.success({ title: "Withdraw Successfully!", }); }) .catch((error) => { State.update({ isError: true, isLoading: false, loadingMsg: error, }); toast?.dismiss(toastId); toast?.fail({ title: "Withdraw Failed!", text: error?.message?.includes("user rejected transaction") ? "User rejected transaction" : error?.message ?? "", }); }); }) }) }; useEffect(() => { if (!sender || !token0 || !token1) return; [ { symbol: token0, address: addresses[token0], decimals: decimals0 }, { symbol: token1, address: addresses[token1], decimals: decimals1 }, ].map(updateBalance); updateExitBalance() }, [sender, token0, token1]); useEffect(() => { if (amount0) { handleTokenChange(amount0, token0); } }, [data]); return ( <DetailWrapper> <FilterButtonList> <FilterButton className={isDeposit ? 'isActive' : ''} onClick={() => changeMode(true)}>Deposit</FilterButton> <FilterButton className={!isDeposit ? 'isActive' : ''} onClick={() => changeMode(false)}>Withdraw</FilterButton> </FilterButtonList> { detailLoading ? ( <div style={{ padding: "30px 0 45px" }}> <Widget props={{ color: "#999" }} src="bluebiu.near/widget/Liquidity.Bridge.Loading" /> </div> ) : ( <> { isDeposit ? <> <Row className="price-input"> <Column> <InputWrap className={Number(amount0) > Number(balances[token0]) ? "inSufficient" : ""}> <Input value={amount0} type="number" onChange={(e) => handleTokenChange(e.target.value, token0)} /> <InputSuffix> <img src={ICON_VAULT_MAP[token0]} alt={token0} /> <span>{token0}</span> </InputSuffix> </InputWrap> <PriceWrap> <TotalPrice>${balance0}</TotalPrice> <BalancePrice>Balance:<span onClick={() => handleMax(true)}>{Big(balances[token0] ?? 0).toFixed(6)}</span> {token0}</BalancePrice> </PriceWrap> </Column> <Column> <InputWrap className={Number(amount1) > Number(balances[token1]) ? "inSufficient" : ""}> <Input value={amount1} type="number" onChange={(e) => handleTokenChange(e.target.value, token1)} /> <InputSuffix> <img src={ICON_VAULT_MAP[token1]} alt={token1} /> <span>{token1}</span> </InputSuffix> </InputWrap> <PriceWrap> <TotalPrice>${balance1}</TotalPrice> <BalancePrice>Balance:<span onClick={() => handleMax(false)}>{Big(balances[token1] ?? 0).toFixed(6)}</span> {token1}</BalancePrice> </PriceWrap> </Column> </Row> <StyledButtonList> {isInSufficient && <StyledButton disabled>InSufficient Balance</StyledButton>} { !isInSufficient && (isToken0Approved && isToken1Approved && !isToken0Approving && !isToken1Approving ? ( <StyledButton disabled={isLoading || !amount0 || !amount1} onClick={handleDeposit}> { isLoading ? ( <Widget src="bluebiu.near/widget/Liquidity.Bridge.Loading" /> ) : ( "Deposit" ) } </StyledButton> ) : ( <> <StyledButton disabled={isToken0Approved || isToken0Approving} onClick={() => handleApprove(true)}>{ isToken0Approving ? ( <Widget src="bluebiu.near/widget/Liquidity.Bridge.Loading" /> ) : ( <> {isToken0Approved ? "Approved" : "Approve"} {token0} </> )} </StyledButton> <StyledButton disabled={isToken1Approved || isToken1Approving} onClick={() => handleApprove(false)}>{ isToken1Approving ? ( <Widget src="bluebiu.near/widget/Liquidity.Bridge.Loading" /> ) : ( <> {isToken1Approved ? "Approved" : "Approve"} {token1} </> )} </StyledButton> </> )) } </StyledButtonList> </> : <> <Row className="price-input"> <Column> <InputWrap className={Number(exitAmount0) > Number(exitBalances[token0]) ? "inSufficient" : ""}> <Input value={exitAmount0} type="number" onChange={(e) => handleExitAmountChange(e.target.value, token0)} /> <InputSuffix> <img src={ICON_VAULT_MAP[token0]} alt={token0} /> <span>{token0}</span> </InputSuffix> </InputWrap> <PriceWrap> {/* <TotalPrice>${balance0}</TotalPrice> */} <BalancePrice>Balance:<span onClick={() => handleMax(true)}>{Big(exitBalances[token0] ?? 0).toFixed(6)}</span> {token0}</BalancePrice> </PriceWrap> </Column> <Column> <InputWrap className={Number(exitAmount1) > Number(exitBalances[token1]) ? "inSufficient" : ""}> <Input value={exitAmount1} type="number" onChange={(e) => handleExitAmountChange(e.target.value, token1)} /> <InputSuffix> <img src={ICON_VAULT_MAP[token1]} alt={token1} /> <span>{token1}</span> </InputSuffix> </InputWrap> <PriceWrap> <TotalPrice>${balance1}</TotalPrice> <BalancePrice>Balance:<span onClick={() => handleMax(false)}>{Big(exitBalances[token1] ?? 0).toFixed(6)}</span> {token1}</BalancePrice> </PriceWrap> </Column> </Row> <StyledButtonList> <StyledButton disabled={isWithdrawInsufficient || isLoading || !exitAmount0 || !exitAmount1} onClick={handleWithdraw} > {isLoading ? ( <Widget src="bluebiu.near/widget/Liquidity.Bridge.Loading" /> ) : ( <> {isWithdrawInsufficient ? "InSufficient Balance" : "Withdraw"} </> )} </StyledButton> </StyledButtonList> </> } </> ) } </DetailWrapper> )