diff --git a/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-1.svg b/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-1.svg new file mode 100644 index 00000000..aef50769 --- /dev/null +++ b/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-1.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-2.svg b/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-2.svg new file mode 100644 index 00000000..9e0c7494 --- /dev/null +++ b/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-3.svg b/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-3.svg new file mode 100644 index 00000000..343abd59 --- /dev/null +++ b/apps/dcellar-web-ui/public/images/toolbox/icon-toolbox-bg-3.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/dcellar-web-ui/public/js/iconfont_v0.6.min.js b/apps/dcellar-web-ui/public/js/iconfont_v0.6.min.js deleted file mode 100644 index 527e0b61..00000000 --- a/apps/dcellar-web-ui/public/js/iconfont_v0.6.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e){var t,n,d,o,i,a,r='';function c(){i||(i=!0,d())}t=function(){var e,t,n;(n=document.createElement("div")).innerHTML=r,r=null,(t=n.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(n=document.body).firstChild?(t=n.firstChild).parentNode.insertBefore(e,t):n.appendChild(e))},document.addEventListener?["complete","loaded","interactive"].indexOf(document.readyState)>-1?setTimeout(t,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),t()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(d=t,o=e.document,i=!1,(a=function(){try{o.documentElement.doScroll("left")}catch(e){return void setTimeout(a,50)}c()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,c())})}(window); diff --git a/apps/dcellar-web-ui/public/js/iconfont_v0.5.min.js b/apps/dcellar-web-ui/public/js/iconfont_v0.7.min.js similarity index 97% rename from apps/dcellar-web-ui/public/js/iconfont_v0.5.min.js rename to apps/dcellar-web-ui/public/js/iconfont_v0.7.min.js index 527e0b61..5c742dd6 100644 --- a/apps/dcellar-web-ui/public/js/iconfont_v0.5.min.js +++ b/apps/dcellar-web-ui/public/js/iconfont_v0.7.min.js @@ -1 +1 @@ -!function(e){var t,n,d,o,i,a,r='';function c(){i||(i=!0,d())}t=function(){var e,t,n;(n=document.createElement("div")).innerHTML=r,r=null,(t=n.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(n=document.body).firstChild?(t=n.firstChild).parentNode.insertBefore(e,t):n.appendChild(e))},document.addEventListener?["complete","loaded","interactive"].indexOf(document.readyState)>-1?setTimeout(t,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),t()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(d=t,o=e.document,i=!1,(a=function(){try{o.documentElement.doScroll("left")}catch(e){return void setTimeout(a,50)}c()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,c())})}(window); +!function(e){var t,n,d,o,i,a,r='';function c(){i||(i=!0,d())}t=function(){var e,t,n;(n=document.createElement("div")).innerHTML=r,r=null,(t=n.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(n=document.body).firstChild?(t=n.firstChild).parentNode.insertBefore(e,t):n.appendChild(e))},document.addEventListener?["complete","loaded","interactive"].indexOf(document.readyState)>-1?setTimeout(t,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),t()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(d=t,o=e.document,i=!1,(a=function(){try{o.documentElement.doScroll("left")}catch(e){return void setTimeout(a,50)}c()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,c())})}(window); \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/components/layout/Nav/index.tsx b/apps/dcellar-web-ui/src/components/layout/Nav/index.tsx index 2c48e095..16bf4436 100644 --- a/apps/dcellar-web-ui/src/components/layout/Nav/index.tsx +++ b/apps/dcellar-web-ui/src/components/layout/Nav/index.tsx @@ -32,6 +32,11 @@ const MENU_ITEMS = [ text: 'Accounts', trackId: 'dc.main.nav.accounts.click', }, + { + icon: 'toolbox', + text: 'Toolbox', + trackId: 'dc.main.nav.toolbox.click', + }, ]; const ASIDE = [ diff --git a/apps/dcellar-web-ui/src/constants/paths.ts b/apps/dcellar-web-ui/src/constants/paths.ts index bd639a9a..c006b277 100644 --- a/apps/dcellar-web-ui/src/constants/paths.ts +++ b/apps/dcellar-web-ui/src/constants/paths.ts @@ -10,6 +10,7 @@ export const InternalRoutePaths = { pricing_calculator: '/pricing-calculator', accounts: '/accounts', dashboard: '/dashboard', + toolbox: '/toolbox', }; export const NODEREAL_URL = 'https://nodereal.io'; \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/context/GlobalContext/PageProtect.tsx b/apps/dcellar-web-ui/src/context/GlobalContext/PageProtect.tsx index 1e7743b1..c59c28a6 100644 --- a/apps/dcellar-web-ui/src/context/GlobalContext/PageProtect.tsx +++ b/apps/dcellar-web-ui/src/context/GlobalContext/PageProtect.tsx @@ -7,8 +7,10 @@ import { isRightChain } from '@/modules/wallet/utils/isRightChain'; import { BSC_CHAIN_ID, GREENFIELD_CHAIN_ID } from '@/base/env'; import { WrongNetworkModal } from '@/components/WrongNetworkModal'; +// protect: GNFD chain, GNFD & BSC chain and no protect. const protectGNFDPaths = ['/buckets', '/buckets/[...path]', '/groups', '/accounts']; -const noProtectPaths = ['/', '/terms', '/pricing-calculator']; +const noProtectPaths = ['/', '/terms', '/pricing-calculator', '/tool-box']; + // TODO unify the wallet page protect export const PageProtect: React.FC = ({ children }) => { const { chain } = useNetwork(); diff --git a/apps/dcellar-web-ui/src/context/GlobalContext/WalletBalanceContext.tsx b/apps/dcellar-web-ui/src/context/GlobalContext/WalletBalanceContext.tsx index 866c0fdb..6fa6ce46 100644 --- a/apps/dcellar-web-ui/src/context/GlobalContext/WalletBalanceContext.tsx +++ b/apps/dcellar-web-ui/src/context/GlobalContext/WalletBalanceContext.tsx @@ -3,6 +3,8 @@ import { Address, useBalance, useNetwork } from 'wagmi'; import { BSC_CHAIN_ID, GREENFIELD_CHAIN_ID } from '@/base/env'; import { useAppSelector } from '@/store'; +import { CRYPTOCURRENCY_DISPLAY_PRECISION } from '@/modules/wallet/constants'; +import { BN } from '@/utils/math'; type TChainBalance = { chainId: number; @@ -51,13 +53,13 @@ export const WalletBalanceProvider: React.FC = ({ children }) => { chainId: BSC_CHAIN_ID, isLoading: isBscLoading, isError: isBscError, - availableBalance: bscBalance?.formatted, + availableBalance: BN(bscBalance?.formatted ?? 0).dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1).toString(), }, { chainId: GREENFIELD_CHAIN_ID, isLoading: isGnfdLoading, isError: isGnfdError, - availableBalance: gnfdBalance?.formatted, + availableBalance: BN(gnfdBalance?.formatted ?? 0).dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1).toString(), }, ], }; diff --git a/apps/dcellar-web-ui/src/facade/wallet.ts b/apps/dcellar-web-ui/src/facade/wallet.ts index 2d69d174..28844562 100644 --- a/apps/dcellar-web-ui/src/facade/wallet.ts +++ b/apps/dcellar-web-ui/src/facade/wallet.ts @@ -1,5 +1,10 @@ import { Connector } from 'wagmi'; import { signTypedDataV4 } from '@/utils/coder'; +import { ethers } from 'ethers'; +import { ErrorResponse, commonFault } from './error'; +import { resolve } from './common'; +import { BN } from '@/utils/math'; +import BigNumber from 'bignumber.js'; export const signTypedDataCallback = (connector: Connector) => { return async (addr: string, message: string) => { @@ -7,3 +12,55 @@ export const signTypedDataCallback = (connector: Connector) => { return await signTypedDataV4(provider, addr, message); }; }; + +export const calTransferInFee = async ( + params: { + amount: string, + crossChainContractAddress: string; + tokenHubContract: string + crossChainAbi: any; + tokenHubAbi: any; + address: string; + }, + signer: ethers.providers.JsonRpcSigner, + provider: ethers.providers.JsonRpcProvider | ethers.providers.FallbackProvider, +): Promise => { + const crossChainContract = new ethers.Contract( + params.crossChainContractAddress, + params.crossChainAbi, + signer!, + ); + const [fee, error1] = await crossChainContract.getRelayFees().then(resolve, commonFault); + if (error1) return [null, error1]; + const [relayFee, ackRelayFee] = fee; + const relayerFee = relayFee.add(ackRelayFee); + const fData = await provider.getFeeData(); + const amountInFormat = ethers.utils.parseEther(String(params.amount)); + const transferInAmount = amountInFormat; + + const totalAmount = amountInFormat.add(ackRelayFee).add(relayFee); + + const tokenHubContract = new ethers.Contract( + params.tokenHubContract, + params.tokenHubAbi, + signer!, + ); + + const [estimateGas, error2] = await tokenHubContract.estimateGas.transferOut( + params.address, + transferInAmount, + { + value: totalAmount, + }, + ).then(resolve, commonFault); + if (!estimateGas || error2) return [null, error2]; + + const gasFee = fData.gasPrice && estimateGas.mul(fData.gasPrice); + + const finalData = { + gasFee: BN(gasFee ? ethers.utils.formatEther(gasFee) : '0'), + relayerFee: BN(ethers.utils.formatEther(relayerFee)), + }; + + return [finalData, null]; +} \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/modules/toolbox/components/Common.tsx b/apps/dcellar-web-ui/src/modules/toolbox/components/Common.tsx new file mode 100644 index 00000000..d849f837 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/toolbox/components/Common.tsx @@ -0,0 +1,44 @@ +import { Center, CircleProps, Flex, FlexProps, LinkProps, Tooltip } from '@totejs/uikit'; + +export const Card = ({ children, ...props }: FlexProps) => { + return ( + + {children} + + ); +}; + +export const CircleLink = ({ children, href, title, ...props }: CircleProps & LinkProps) => { + return ( + +
+ {children} +
+
+ ); +}; diff --git a/apps/dcellar-web-ui/src/modules/toolbox/components/TellUsCard.tsx b/apps/dcellar-web-ui/src/modules/toolbox/components/TellUsCard.tsx new file mode 100644 index 00000000..bd0ebd1e --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/toolbox/components/TellUsCard.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Card } from './Common'; +import { Box, Text, Tooltip } from '@totejs/uikit'; +import { DCButton } from '@/components/common/DCButton'; +import { assetPrefix } from '@/base/env'; + +export const TellUsCard = () => { + const onNavigateExternal = (url: string) => { + window.open(url, '_blank', 'noreferrer'); + }; + return ( + + + Start Building with DCellar Now + + + + DCellar offers a full set of open source toolkits for developers to start build on + Greenfield at ease. + + + + onNavigateExternal('#')} + > + Explorer + + + + ); +}; diff --git a/apps/dcellar-web-ui/src/modules/toolbox/components/UploadkitCard.tsx b/apps/dcellar-web-ui/src/modules/toolbox/components/UploadkitCard.tsx new file mode 100644 index 00000000..46b03b3c --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/toolbox/components/UploadkitCard.tsx @@ -0,0 +1,36 @@ +import { IconFont } from '@/components/IconFont'; +import { Badge, Flex, Text } from '@totejs/uikit'; +import { Card, CircleLink } from './Common'; + +export const UploadKitCard = () => { + return ( + + + + + Greenfield UploadKit + + + + + + + + + + + + + + Component + + + Greenfield Upload UIKit is offered by NodeReal, it's fully open sourced, developers can + easily integrate into their WebUI dApps. + + + ); +}; diff --git a/apps/dcellar-web-ui/src/modules/toolbox/page/index.tsx b/apps/dcellar-web-ui/src/modules/toolbox/page/index.tsx new file mode 100644 index 00000000..a0c15750 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/toolbox/page/index.tsx @@ -0,0 +1,19 @@ +import { Box, Flex, Text } from '@totejs/uikit'; +import { TellUsCard } from '../components/TellUsCard'; +import { UploadKitCard } from '../components/UploadkitCard'; + +export const ToolBoxPage = () => { + return ( + <> + + + Toolbox + + + + + + + + ); +}; \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/modules/wallet/Send/index.tsx b/apps/dcellar-web-ui/src/modules/wallet/Send/index.tsx index 32150cdb..fe0bd064 100644 --- a/apps/dcellar-web-ui/src/modules/wallet/Send/index.tsx +++ b/apps/dcellar-web-ui/src/modules/wallet/Send/index.tsx @@ -414,8 +414,10 @@ export const Send = memo(function Send() { register={register} disabled={isSubmitting} watch={watch} + bankBalance={bankBalance} feeData={feeData} setValue={setValue} + settlementFee={settlementFee} maxDisabled={isLoading} /> {isShowFee() ? ( diff --git a/apps/dcellar-web-ui/src/modules/wallet/TransferIn/index.tsx b/apps/dcellar-web-ui/src/modules/wallet/TransferIn/index.tsx index 43f4e560..2dded9bb 100644 --- a/apps/dcellar-web-ui/src/modules/wallet/TransferIn/index.tsx +++ b/apps/dcellar-web-ui/src/modules/wallet/TransferIn/index.tsx @@ -1,11 +1,9 @@ import { Box, Divider, Flex, useDisclosure } from '@totejs/uikit'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useNetwork } from 'wagmi'; +import { memo, useCallback, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { ethers } from 'ethers'; import { isEmpty } from 'lodash-es'; import { useRouter } from 'next/router'; -import BigNumber from 'bignumber.js'; import { ChainBox } from '../components/ChainBox'; import Amount from '../components/Amount'; @@ -16,9 +14,7 @@ import Container from '../components/Container'; import { BSC_CHAIN_ID, BSC_EXPLORER_URL, GREENFIELD_CHAIN_ID } from '@/base/env'; import { WalletButton } from '../components/WalletButton'; import { Fee } from '../components/Fee'; -import { EOperation, TCalculateGas, TFeeData, TTransferInFromValues } from '../type'; -import { CROSS_CHAIN_ABI, INIT_FEE_DATA, TOKENHUB_ABI, WalletOperationInfos } from '../constants'; -import { isRightChain } from '../utils/isRightChain'; +import { TTransferInFromValues } from '../type'; import { GAClick } from '@/components/common/GATracker'; import { useAppSelector } from '@/store'; import { useChainsBalance } from '@/context/GlobalContext/WalletBalanceContext'; @@ -27,26 +23,16 @@ import { removeTrailingSlash } from '@/utils/string'; import { broadcastFault } from '@/facade/error'; import { Faucet } from '../components/Faucet'; import { LargeAmountTip } from '../components/LargeAmountTip'; -import { useEthersProvider, useEthersSigner } from '../hooks'; +import { useTransferInFee } from '../hooks'; interface TransferInProps {} - export const TransferIn = memo(function TransferIn() { - const { - TOKEN_HUB_CONTRACT_ADDRESS: APOLLO_TOKEN_HUB_CONTRACT_ADDRESS, - CROSS_CHAIN_CONTRACT_ADDRESS: APOLLO_CROSS_CHAIN_CONTRACT_ADDRESS, - } = useAppSelector((root) => root.apollo); const { transType } = useAppSelector((root) => root.wallet); const { isOpen, onClose, onOpen } = useDisclosure(); const [status, setStatus] = useState('success'); const [errorMsg, setErrorMsg] = useState('Oops, something went wrong'); const router = useRouter(); const [viewTxUrl, setViewTxUrl] = useState(''); - const { loginAccount: address } = useAppSelector((root) => root.persist); - const provider = useEthersProvider({ chainId: BSC_CHAIN_ID }); - const signer = useEthersSigner({chainId: BSC_CHAIN_ID}) - const [feeData, setFeeData] = useState(INIT_FEE_DATA); - const [isGasLoading, setIsGasLoading] = useState(false); const { all } = useChainsBalance(); const { handleSubmit, @@ -59,68 +45,22 @@ export const TransferIn = memo(function TransferIn() { } = useForm({ mode: 'all', }); - const { chain } = useNetwork(); - const curInfo = WalletOperationInfos[transType]; - const isRight = useMemo(() => { - return isRightChain(chain?.id, curInfo?.chainId); - }, [chain?.id, curInfo?.chainId]); const inputAmount = getValues('amount'); const balance = useMemo(() => { return all.find((item) => item.chainId === BSC_CHAIN_ID)?.availableBalance || ''; }, [all]); - const getFee = useCallback( - async ({ amountIn, type = 'content_value' }: { amountIn: string; type?: TCalculateGas }) => { - if (signer && amountIn) { - try { - setIsGasLoading(true); - const crossChainContract = new ethers.Contract( - APOLLO_CROSS_CHAIN_CONTRACT_ADDRESS || '', - CROSS_CHAIN_ABI, - signer!, - ); - const [relayFee, ackRelayFee] = await crossChainContract.getRelayFees(); - const relayerFee = relayFee.add(ackRelayFee); - const fData = await provider.getFeeData(); - const amountInFormat = ethers.utils.parseEther(String(amountIn)); - - // bsc simulate gas fee need real amount. - const transferInAmount = - type === 'content_value' - ? amountInFormat - : amountInFormat.sub(ackRelayFee).sub(relayFee); - const totalAmount = - type === 'content_value' - ? amountInFormat.add(ackRelayFee).add(relayFee) - : amountInFormat; - const tokenHubContract = new ethers.Contract( - APOLLO_TOKEN_HUB_CONTRACT_ADDRESS || '', - TOKENHUB_ABI, - signer!, - ); - const estimateGas = await tokenHubContract.estimateGas.transferOut( - address, - transferInAmount, - { - value: totalAmount, - }, - ); - const gasFee = fData.gasPrice && estimateGas.mul(fData.gasPrice); - const finalData = { - gasFee: BigNumber(gasFee ? ethers.utils.formatEther(gasFee) : '0'), - relayerFee: BigNumber(ethers.utils.formatEther(relayerFee)), - }; - setIsGasLoading(false); - setFeeData(finalData); - } catch (e) { - // eslint-disable-next-line no-console - console.log('getGas error', e); - setIsGasLoading(false); - } - } - }, - [address, provider, signer], - ); + const { + isLoading: isGasLoading, + feeData, + signer, + getFee, + loginAccount: address, + tokenHubContract, + tokenHubAbi, + crossChainAbi, + crossChainContract, + } = useTransferInFee(); const isShowFee = useCallback(() => { return isEmpty(errors) && !isEmpty(inputAmount); @@ -131,22 +71,20 @@ export const TransferIn = memo(function TransferIn() { onOpen(); try { - const crossChainContract = new ethers.Contract( - APOLLO_CROSS_CHAIN_CONTRACT_ADDRESS || '', - CROSS_CHAIN_ABI, - signer!, - ); - const tokenHubContract = new ethers.Contract( - APOLLO_TOKEN_HUB_CONTRACT_ADDRESS || '', - TOKENHUB_ABI, + const cInstance = new ethers.Contract( + crossChainContract, + crossChainAbi, signer!, ); + const tInstance = new ethers.Contract(tokenHubContract, tokenHubAbi, signer!); + const transferInAmount = data.amount; const amount = ethers.utils.parseEther(transferInAmount.toString()); - const [relayFee, ackRelayFee] = await crossChainContract.getRelayFees(); + const [relayFee, ackRelayFee] = await cInstance.getRelayFees(); const relayerFee = relayFee.add(ackRelayFee); const totalAmount = relayerFee.add(amount); - const tx = await tokenHubContract.transferOut(address, amount, { + + const tx = await tInstance.transferOut(address, amount, { value: totalAmount, }); @@ -174,19 +112,6 @@ export const TransferIn = memo(function TransferIn() { onClose(); }; - useEffect(() => { - if ( - !isEmpty(errors) || - !isRight || - isEmpty(inputAmount) || - transType !== EOperation.transfer_in - ) { - return; - } - - getFee({ amountIn: inputAmount }); - }, [getFee, isRight, transType, inputAmount, errors]); - return ( <> @@ -207,7 +132,7 @@ export const TransferIn = memo(function TransferIn() { watch={watch} feeData={feeData} setValue={setValue} - getGasFee={getFee} + refreshFee={getFee} maxDisabled={isGasLoading} /> {isShowFee() ? ( diff --git a/apps/dcellar-web-ui/src/modules/wallet/components/Amount.tsx b/apps/dcellar-web-ui/src/modules/wallet/components/Amount.tsx index dbd1f307..a004f83b 100644 --- a/apps/dcellar-web-ui/src/modules/wallet/components/Amount.tsx +++ b/apps/dcellar-web-ui/src/modules/wallet/components/Amount.tsx @@ -10,7 +10,7 @@ import { Link, Text, } from '@totejs/uikit'; -import React, { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useNetwork } from 'wagmi'; import { isEmpty } from 'lodash-es'; import BigNumber from 'bignumber.js'; @@ -19,10 +19,11 @@ import { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch } from 'rea import { CRYPTOCURRENCY_DISPLAY_PRECISION, DECIMAL_NUMBER, + DefaultTransferFee, MIN_AMOUNT, WalletOperationInfos, } from '../constants'; -import { EOperation, GetFeeType, TFeeData, TWalletFromValues } from '../type'; +import { EOperation, TFeeData, TWalletFromValues } from '../type'; import { useChainsBalance } from '@/context/GlobalContext/WalletBalanceContext'; import { useAppSelector } from '@/store'; import { selectBnbPrice } from '@/store/slices/global'; @@ -32,27 +33,35 @@ import { currencyFormatter } from '@/utils/formatter'; import { BN } from '@/utils/math'; import { IconFont } from '@/components/IconFont'; import { displayTokenSymbol } from '@/utils/wallet'; +import { isRightChain } from '../utils/isRightChain'; +import { MaxButton } from './MaxButton'; +import { ErrorResponse } from '@/facade/error'; +import { setMaxAmount } from '../utils/common'; type AmountProps = { disabled: boolean; feeData: TFeeData; errors: FieldErrors; + bankBalance?: string; + settlementFee?: string; + refreshFee?: (transferAmount: string) => Promise; register: UseFormRegister; watch: UseFormWatch; setValue: UseFormSetValue; - getGasFee?: GetFeeType; maxDisabled?: boolean; txType?: TxType; balance: string; }; const AmountErrors = { + validateWithdrawStaticBalance: "The payment account doesn't have enough balance to pay settlement fee.", + validateWithdrawBankBalance: "The owner account doesn't have enough balance to pay gas fee.", validateBalance: 'Insufficient balance.', validateFormat: 'Invalid amount.', - validateNum: `The maximum precision is ${CRYPTOCURRENCY_DISPLAY_PRECISION} digits.`, + validatePrecision: `The maximum precision is ${CRYPTOCURRENCY_DISPLAY_PRECISION} digits.`, required: 'Amount is required.', min: 'Please enter a minimum amount of 0.00000001.', - withdrawError: ( + validateWithdrawMaxAmountError: ( <> No withdrawals allowed over 100 {displayTokenSymbol()}.{' '} { const bnbPrice = useAppSelector(selectBnbPrice); const { transType } = useAppSelector((root) => root.wallet); @@ -90,9 +103,9 @@ export const Amount = ({ const { gasFee, relayerFee } = feeData; const { isLoading } = useChainsBalance(); const { chain } = useNetwork(); - // const isRight = useMemo(() => { - // return isRightChain(chain?.id, curInfo?.chainId); - // }, [chain?.id, curInfo?.chainId]); + const isShowMaxButton = useMemo(() => { + return isRightChain(chain?.id, curInfo?.chainId); + }, [chain?.id, curInfo?.chainId]); const isSendPage = transType === 'send'; const Balance = useCallback(() => { @@ -113,17 +126,78 @@ export const Amount = ({ ); }, [balance, bnbPrice, curInfo?.chainName, isLoading]); - // const onMaxClick = async () => { - // if (balance && feeData) { - // getGasFee && (await getGasFee({ amountIn: balance?.formatted, type: 'total_value' })); - // const availableBalance = BigNumber(balance.formatted) - // .minus(feeData.gasFee) - // .minus(feeData.relayerFee) - // .dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1); - // const availableStr = availableBalance.toString(DECIMAL_NUMBER); - // setValue('amount', availableStr, { shouldValidate: true }); - // } - // }; + const onMaxClick = async () => { + if (!balance || !feeData) return setValue('amount', '0', { shouldValidate: true }); + if (txType === 'withdraw_from_payment_account') { + const cal = BN(balance).minus(settlementFee || '0').dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1).toString(); + const maxAmount = BN(cal).lt(0) ? '0' : cal; + + return setValue('amount', maxAmount, { + shouldValidate: true, + }); + } + + if (transType === 'transfer_in' && refreshFee) { + const [realTimeFee, error] = await refreshFee( + BN(balance).minus(DefaultTransferFee.transfer_in.total).toString(), + ); + realTimeFee && setMaxAmount(balance, realTimeFee, setValue); + return; + } + + setMaxAmount(balance, feeData, setValue); + }; + + const validateBalance = (val: string) => { + if (txType === 'withdraw_from_payment_account') { + return BN(balance).isGreaterThanOrEqualTo(BN(val).plus(settlementFee || '0')); + } + + let totalAmount = BigNumber(0); + const balanceVal = BigNumber(balance || 0); + if (transType === EOperation.send) { + totalAmount = + gasFee.toString() === '0' + ? BigNumber(val).plus(BigNumber(defaultFee)) + : BigNumber(val).plus(gasFee); + } else { + totalAmount = + gasFee.toString() === '0' && relayerFee.toString() === '0' + ? BigNumber(val).plus(BigNumber(defaultFee)) + : BigNumber(val).plus(gasFee).plus(relayerFee); + } + + return balanceVal.isGreaterThanOrEqualTo(totalAmount); + }; + + const validatePrecision = (val: string) => { + const precisionStr = val.split('.')[1]; + return !precisionStr || precisionStr.length <= CRYPTOCURRENCY_DISPLAY_PRECISION; + }; + const validateWithdrawBankBalance = () => { + if (txType !== 'withdraw_from_payment_account') return true; + return BN(bankBalance as string).isGreaterThanOrEqualTo(gasFee); + }; + const validateWithdrawMaxAmountError = (val: string) => { + if (txType !== 'withdraw_from_payment_account') return true; + return BN(val).lt(100); + }; + const validateWithdrawStaticBalance = (val: string) => { + if (txType !== 'withdraw_from_payment_account') return true; + return BN(balance).isGreaterThanOrEqualTo(BN(settlementFee || '0').plus(val)); + } + const onPaste = (e: any) => { + e.stopPropagation(); + e.preventDefault(); + + const clipboardData = e.clipboardData || window.clipboardData; + const pastedData = clipboardData.getData('Text'); + if (/^\d+(\.\d+)?$/.test(pastedData)) { + return setValue('amount', pastedData, { shouldValidate: true }); + } + e.preventDefault(); + }; + watch('amount'); return ( <> @@ -137,18 +211,7 @@ export const Amount = ({ > Amount - {/* {isRight && ( - - )} */} + {isShowMaxButton && } @@ -170,50 +233,18 @@ export const Amount = ({ e.preventDefault(); } }} - onPaste={(e) => { - e.stopPropagation(); - e.preventDefault(); - - const clipboardData = e.clipboardData || window.clipboardData; - const pastedData = clipboardData.getData('Text'); - if (/^\d+(\.\d+)?$/.test(pastedData)) { - setValue('amount', pastedData, { shouldValidate: true }); - - return; - } - e.preventDefault(); - }} + onPaste={onPaste} onWheel={(event) => event.currentTarget.blur()} color={!isEmpty(errors?.amount) ? '#EA412E' : '#1E2026'} {...register('amount', { required: true, min: MIN_AMOUNT, validate: { - validateBalance: (val: string) => { - let totalAmount = BigNumber(0); - const balanceVal = BigNumber(balance || 0); - if (transType === EOperation.send) { - totalAmount = - gasFee.toString() === '0' - ? BigNumber(val).plus(BigNumber(defaultFee)) - : BigNumber(val).plus(gasFee); - } else { - totalAmount = - gasFee.toString() === '0' && relayerFee.toString() === '0' - ? BigNumber(val).plus(BigNumber(defaultFee)) - : BigNumber(val).plus(gasFee).plus(relayerFee); - } - return totalAmount.comparedTo(balanceVal) <= 0; - }, - validateNum: (val: string) => { - const precisionStr = val.split('.')[1]; - - return !precisionStr || precisionStr.length <= CRYPTOCURRENCY_DISPLAY_PRECISION; - }, - withdrawError: (val: string) => { - if (!txType || txType !== 'withdraw_from_payment_account') return true; - return BN(val).lt(100); - }, + validateWithdrawBankBalance, + validateWithdrawStaticBalance, + validateBalance, + validatePrecision, + validateWithdrawMaxAmountError, }, })} /> diff --git a/apps/dcellar-web-ui/src/modules/wallet/components/Fee.tsx b/apps/dcellar-web-ui/src/modules/wallet/components/Fee.tsx index 3fa77820..6836a7cc 100644 --- a/apps/dcellar-web-ui/src/modules/wallet/components/Fee.tsx +++ b/apps/dcellar-web-ui/src/modules/wallet/components/Fee.tsx @@ -6,6 +6,7 @@ import { EOperation, TFeeData } from '../type'; import { CRYPTOCURRENCY_DISPLAY_PRECISION, DECIMAL_NUMBER, + DefaultTransferFee, FIAT_CURRENCY_DISPLAY_PRECISION, INIT_FEE_DATA, } from '../constants'; @@ -20,19 +21,6 @@ import { renderFee } from '@/utils/common'; import { displayTokenSymbol } from '@/utils/wallet'; import { isEmpty } from 'lodash-es'; -const DefaultFee = { - // TODO temp down limit fee - transfer_in: 0.00008 + 0.002, - transfer_out: 0.000006 + 0.001, - send: 0.000006, -}; -const DefaultGasRelayerFee = { - // TODO temp down limit fee - transfer_in: { gasFee: 0.00008, relayerFee: 0.002 }, - transfer_out: { gasFee: 0.000006, relayerFee: 0.001 }, - send: { gasFee: 0, relayerFee: 0 }, -}; - interface FeeProps { amount: string; showSettlement?: boolean; @@ -60,8 +48,7 @@ export const Fee = memo(function Fee({ const { price: exchangeRate } = useAppSelector((root) => root.global.bnb); const { transType } = useAppSelector((root) => root.wallet); const { gasFee, relayerFee } = feeData; - const defaultFee = DefaultFee[transType]; - const defaultGasRelayerFee = DefaultGasRelayerFee[transType]; + const defaultTransferFee = DefaultTransferFee[transType]; const totalFee = gasFee.plus(relayerFee); const isShowDefault = gasFee.toString() === '0' && relayerFee.toString() === '0'; const feeUsdPrice = totalFee && totalFee.times(BigNumber(bnbPrice)); @@ -84,7 +71,7 @@ export const Fee = memo(function Fee({ //show defalut fee if cannot get fee data in 3000ms const defaultFeeUsdPrice = currencyFormatter( - BigNumber(defaultFee) + BigNumber(defaultTransferFee.total) .times(BigNumber(bnbPrice)) .dp(FIAT_CURRENCY_DISPLAY_PRECISION) .toString(DECIMAL_NUMBER), @@ -92,7 +79,7 @@ export const Fee = memo(function Fee({ const TotalFeeContent = useMemo(() => { let total = totalFee; if (isShowDefault) { - total = BigNumber(defaultFee); + total = BigNumber(defaultTransferFee.total); return `~${total .dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1) .toString(DECIMAL_NUMBER)} ${TOKEN_SYMBOL} (${defaultFeeUsdPrice})`; @@ -100,7 +87,7 @@ export const Fee = memo(function Fee({ return `${totalFee .dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1) .toString(DECIMAL_NUMBER)} ${TOKEN_SYMBOL} (${formatFeeUsdPrice})`; - }, [TOKEN_SYMBOL, defaultFee, formatFeeUsdPrice, isShowDefault, totalFee, defaultFeeUsdPrice]); + }, [TOKEN_SYMBOL, defaultTransferFee, formatFeeUsdPrice, isShowDefault, totalFee, defaultFeeUsdPrice]); const TotalAmountContent = `${totalAmount} ${TOKEN_SYMBOL} (${formatTotalUsdPrice})`; const TipContent = useMemo(() => { @@ -112,7 +99,7 @@ export const Fee = memo(function Fee({ Gas fee:{' '} {gasFee.toString() === '0' - ? BigNumber(defaultGasRelayerFee.gasFee) + ? BigNumber(defaultTransferFee.gasFee) .dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1) .toString(DECIMAL_NUMBER) : gasFee.dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1).toString(DECIMAL_NUMBER)}{' '} @@ -121,7 +108,7 @@ export const Fee = memo(function Fee({ Relayer fee:{' '} {gasFee.toString() === '0' - ? BigNumber(defaultGasRelayerFee.relayerFee) + ? BigNumber(defaultTransferFee.relayerFee) .dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1) .toString(DECIMAL_NUMBER) : relayerFee.dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1).toString()}{' '} @@ -136,8 +123,7 @@ export const Fee = memo(function Fee({ }, [ transType, gasFee, - defaultGasRelayerFee.gasFee, - defaultGasRelayerFee.relayerFee, + defaultTransferFee, TOKEN_SYMBOL, relayerFee, ]); diff --git a/apps/dcellar-web-ui/src/modules/wallet/components/MaxButton.tsx b/apps/dcellar-web-ui/src/modules/wallet/components/MaxButton.tsx new file mode 100644 index 00000000..4c73caef --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/wallet/components/MaxButton.tsx @@ -0,0 +1,27 @@ +import { DCButton } from '@/components/common/DCButton'; +import React from 'react'; + +type MaxButtonProps = { + disabled?: boolean; + onMaxClick: () => void; +}; +export const MaxButton = ({ disabled = false, onMaxClick }: MaxButtonProps) => { + return ( + + Max + + ); +}; diff --git a/apps/dcellar-web-ui/src/modules/wallet/constants.ts b/apps/dcellar-web-ui/src/modules/wallet/constants.ts index 3015fbca..b49094ca 100644 --- a/apps/dcellar-web-ui/src/modules/wallet/constants.ts +++ b/apps/dcellar-web-ui/src/modules/wallet/constants.ts @@ -59,6 +59,24 @@ export const INIT_FEE_DATA = { relayerFee: BigNumber('0'), }; +export const DefaultTransferFee = { + transfer_in: { + total: 0.00208, + gasFee: 0.00008, + relayerFee: 0.002 + }, + transfer_out: { + total: 0.001006, + gasFee: 0.00006, + relayerFee: 0.001, + }, + send: { + total: 0.00006, + gasFee: 0.00006, + relayerFee: 0, + }, +}; + export const CROSS_CHAIN_ABI = [ { anonymous: false, diff --git a/apps/dcellar-web-ui/src/modules/wallet/hooks.ts b/apps/dcellar-web-ui/src/modules/wallet/hooks.ts index f4bf93b8..8919e160 100644 --- a/apps/dcellar-web-ui/src/modules/wallet/hooks.ts +++ b/apps/dcellar-web-ui/src/modules/wallet/hooks.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useNetwork, usePublicClient, useWalletClient } from 'wagmi'; import BigNumber from 'bignumber.js'; -import { INIT_FEE_DATA, MIN_AMOUNT, WalletOperationInfos } from './constants'; +import { CROSS_CHAIN_ABI, DefaultTransferFee, MIN_AMOUNT, TOKENHUB_ABI, WalletOperationInfos } from './constants'; import { EOperation, TFeeData } from './type'; import { getRelayFeeBySimulate } from './utils/simulate'; import { isRightChain } from './utils/isRightChain'; @@ -12,6 +12,10 @@ import { genTransferOutTx } from './utils/genTransferOutTx'; import { useAppSelector } from '@/store'; import { getClient } from '@/facade'; import { publicClientToProvider, walletClientToSigner } from './utils/ethers'; +import { BSC_CHAIN_ID } from '@/base/env'; +import { calTransferInFee } from '@/facade/wallet'; +import { useAsyncEffect } from 'ahooks'; +import { ErrorResponse } from '@/facade/error'; export const useGetFeeBasic = () => { const { transType } = useAppSelector((root) => root.wallet); @@ -32,7 +36,10 @@ export const useGetFeeBasic = () => { export const useTransferOutFee = () => { const { type, isRight, address } = useGetFeeBasic(); - const [feeData, setFeeData] = useState(INIT_FEE_DATA); + const [feeData, setFeeData] = useState({ + gasFee: BigNumber(DefaultTransferFee['transfer_out'].gasFee), + relayerFee: BigNumber(DefaultTransferFee['transfer_out'].relayerFee), + }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -55,9 +62,9 @@ export const useTransferOutFee = () => { const relayFee = relayFeeInfo.params ? getRelayFeeBySimulate( - relayFeeInfo.params.bscTransferOutAckRelayerFee, - relayFeeInfo.params.bscTransferOutRelayerFee, - ) + relayFeeInfo.params.bscTransferOutAckRelayerFee, + relayFeeInfo.params.bscTransferOutRelayerFee, + ) : '0'; const newData = { @@ -86,7 +93,10 @@ export const useTransferOutFee = () => { export const useSendFee = () => { const { type, address, isRight } = useGetFeeBasic(); - const [feeData, setFeeData] = useState(INIT_FEE_DATA); + const [feeData, setFeeData] = useState({ + gasFee: BigNumber(DefaultTransferFee['send'].gasFee), + relayerFee: BigNumber(DefaultTransferFee['send'].relayerFee), + }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -110,7 +120,7 @@ export const useSendFee = () => { denom: 'BNB', }); setFeeData({ - ...INIT_FEE_DATA, + relayerFee: BigNumber(DefaultTransferFee['send'].relayerFee), gasFee: BigNumber(simulateTxInfo.gasFee), }); @@ -145,3 +155,67 @@ export function useEthersSigner({ chainId }: { chainId?: number } = {}) { const { data: walletClient } = useWalletClient({ chainId }); return useMemo(() => (walletClient ? walletClientToSigner(walletClient) : undefined), [walletClient]); } + +export const useTransferInFee = () => { + const [feeData, setFeeData] = useState({ + gasFee: BigNumber(DefaultTransferFee['transfer_in'].gasFee), + relayerFee: BigNumber(DefaultTransferFee['transfer_in'].relayerFee), + }); + const { loginAccount } = useAppSelector((root) => root.persist); + const [isGasLoading, setIsGasLoading] = useState(false); + const { + TOKEN_HUB_CONTRACT_ADDRESS: APOLLO_TOKEN_HUB_CONTRACT_ADDRESS, + CROSS_CHAIN_CONTRACT_ADDRESS: APOLLO_CROSS_CHAIN_CONTRACT_ADDRESS, + } = useAppSelector((root) => root.apollo); + const provider = useEthersProvider({ chainId: BSC_CHAIN_ID }); + const signer = useEthersSigner({ chainId: BSC_CHAIN_ID }); + const getFee = useCallback( + async (transferAmount: string): Promise => { + if (!signer || !provider) return [null, 'no signer or provider']; + setIsGasLoading(true); + const params = { + amount: transferAmount, + address: loginAccount, + crossChainContractAddress: APOLLO_CROSS_CHAIN_CONTRACT_ADDRESS, + tokenHubContract: APOLLO_TOKEN_HUB_CONTRACT_ADDRESS, + crossChainAbi: CROSS_CHAIN_ABI, + tokenHubAbi: TOKENHUB_ABI, + }; + const [data, error] = await calTransferInFee( + params, + signer, + provider, + ); + setIsGasLoading(false); + if (!data) { + return [null, error] + } + + setFeeData(data); + return [data, error]; + }, + [ + APOLLO_CROSS_CHAIN_CONTRACT_ADDRESS, + APOLLO_TOKEN_HUB_CONTRACT_ADDRESS, + loginAccount, + provider, + signer, + ], + ); + + useAsyncEffect(async () => { + await getFee(MIN_AMOUNT) + }, [getFee]) + + return { + feeData, + isLoading: isGasLoading, + crossChainContract: APOLLO_CROSS_CHAIN_CONTRACT_ADDRESS, + signer, + tokenHubContract: APOLLO_TOKEN_HUB_CONTRACT_ADDRESS, + crossChainAbi: CROSS_CHAIN_ABI, + tokenHubAbi: TOKENHUB_ABI, + loginAccount, + getFee, + } +} \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/modules/wallet/index.tsx b/apps/dcellar-web-ui/src/modules/wallet/index.tsx index 7663b9c4..0c2c05c1 100644 --- a/apps/dcellar-web-ui/src/modules/wallet/index.tsx +++ b/apps/dcellar-web-ui/src/modules/wallet/index.tsx @@ -13,6 +13,7 @@ import styled from '@emotion/styled'; interface WalletProps {} +// TODO: Refactor export const Wallet = memo(function Wallet() { const { transType } = useAppSelector((root) => root.wallet); const router = useRouter(); diff --git a/apps/dcellar-web-ui/src/modules/wallet/type.ts b/apps/dcellar-web-ui/src/modules/wallet/type.ts index 054b48e5..caa5110b 100644 --- a/apps/dcellar-web-ui/src/modules/wallet/type.ts +++ b/apps/dcellar-web-ui/src/modules/wallet/type.ts @@ -2,7 +2,6 @@ import BigNumber from 'bignumber.js'; export type TOperation = 'send' | 'transfer_in' | 'transfer_out'; -export type TCalculateGas = 'content_value' | 'total_value'; export enum EOperation { 'send' = 'send', @@ -31,12 +30,6 @@ export type TSendFromValues = TAmountFieldValue & TAddressFieldValue; export type TWalletFromValues = TTransferInFromValues | TTransferOutFromValues | TSendFromValues; -export type GetFeeType = ({ - amountIn, - type, -}: { - amountIn: string; - type?: TCalculateGas | undefined; -}) => Promise; +export type GetFeeType = (amount: string) => Promise; export type TNormalObject = { [key: string]: string }; diff --git a/apps/dcellar-web-ui/src/modules/wallet/utils/common.ts b/apps/dcellar-web-ui/src/modules/wallet/utils/common.ts new file mode 100644 index 00000000..a5c178d3 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/wallet/utils/common.ts @@ -0,0 +1,15 @@ +import { BN } from '@/utils/math'; +import { TFeeData, TWalletFromValues } from '../type'; +import { CRYPTOCURRENCY_DISPLAY_PRECISION } from '../constants'; +import { UseFormSetValue } from 'react-hook-form'; + +export const setMaxAmount = (balance: string, feeData: TFeeData, setValue: UseFormSetValue) => { + const availableBalance = BN(balance) + .minus(feeData.gasFee) + .minus(feeData.relayerFee) + .dp(CRYPTOCURRENCY_DISPLAY_PRECISION, 1) + .toNumber(); + + const availableStr = availableBalance < 0 ? '0' : availableBalance.toString(); + setValue('amount', availableStr, { shouldValidate: true }); +} \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/pages/_document.tsx b/apps/dcellar-web-ui/src/pages/_document.tsx index 07fd2648..6bc9ef0c 100644 --- a/apps/dcellar-web-ui/src/pages/_document.tsx +++ b/apps/dcellar-web-ui/src/pages/_document.tsx @@ -25,7 +25,7 @@ export default function Document() { __html: `window.__ASSET_PREFIX = ${JSON.stringify(assetPrefix)}`, }} > - + diff --git a/apps/dcellar-web-ui/src/pages/toolbox/index.tsx b/apps/dcellar-web-ui/src/pages/toolbox/index.tsx new file mode 100644 index 00000000..079e6979 --- /dev/null +++ b/apps/dcellar-web-ui/src/pages/toolbox/index.tsx @@ -0,0 +1,5 @@ +import { ToolBoxPage } from '@/modules/toolbox/page'; + +export const ToolBox = () => ; + +export default ToolBox; \ No newline at end of file