diff --git a/src/hooks/transfer/useTokenTransfer.ts b/src/hooks/transfer/useTokenTransfer.ts index 100e45860..d038108f8 100644 --- a/src/hooks/transfer/useTokenTransfer.ts +++ b/src/hooks/transfer/useTokenTransfer.ts @@ -1,27 +1,31 @@ +import { ISubmittableResult } from '@polkadot/types/types'; +import { BN } from '@polkadot/util'; import { $api, $web3 } from 'boot/api'; import { ethers } from 'ethers'; import ABI from 'src/config/abi/ERC20.json'; -import { getTokenBal, isValidEvmAddress, toSS58Address } from 'src/config/web3'; -import { useGasPrice, useNetworkInfo } from 'src/hooks'; +import { buildEvmAddress, getTokenBal, isValidEvmAddress, toSS58Address } from 'src/config/web3'; +import { useAccount, useBalance, useCustomSignature, useGasPrice, useNetworkInfo } from 'src/hooks'; +import { useEthProvider } from 'src/hooks/custom-signature/useEthProvider'; import { ASTAR_SS58_FORMAT, isValidAddressPolkadotAddress, SUBSTRATE_SS58_FORMAT, } from 'src/hooks/helper/plasmUtils'; -import { useAccount } from 'src/hooks/useAccount'; +import { signAndSend } from 'src/hooks/helper/wallet'; import { HistoryTxType } from 'src/modules/account'; import { addTxHistories } from 'src/modules/account/utils/index'; import { sampleEvmWalletAddress } from 'src/modules/gas-api'; -import { getEvmGasCost } from 'src/modules/gas-api/utils/index'; +import { getEvmGas, getEvmGasCost } from 'src/modules/gas-api/utils/index'; import { fetchXcmBalance } from 'src/modules/xcm'; import { Path } from 'src/router'; import { useStore } from 'src/store'; -import { container } from 'src/v2/common'; +import { SubstrateAccount } from 'src/store/general/state'; import { Asset } from 'src/v2/models'; -import { ITokenTransferService } from 'src/v2/services'; -import { Symbols } from 'src/v2/symbols'; import { computed, ref, Ref, watch, watchEffect } from 'vue'; +import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; +import Web3 from 'web3'; +import { TransactionConfig } from 'web3-eth'; import { AbiItem } from 'web3-utils'; export function useTokenTransfer(selectedToken: Ref) { @@ -32,7 +36,18 @@ export function useTokenTransfer(selectedToken: Ref) { const isChecked = ref(false); const store = useStore(); + const { ethProvider } = useEthProvider(); + const { t } = useI18n(); const { currentAccount } = useAccount(); + const { accountData } = useBalance(currentAccount); + + const transferableBalance = computed(() => { + const balance = accountData.value + ? ethers.utils.formatEther(accountData.value.getUsableTransactionBalance().toString()) + : '0'; + return Number(balance); + }); + const { handleResult, handleCustomExtrinsic } = useCustomSignature({}); const { evmGasPrice, selectedGas, @@ -45,10 +60,14 @@ export function useTokenTransfer(selectedToken: Ref) { const route = useRoute(); const router = useRouter(); - const { nativeTokenSymbol, evmNetworkIdx } = useNetworkInfo(); + const { nativeTokenSymbol, evmNetworkIdx, isSupportXvmTransfer } = useNetworkInfo(); const isH160 = computed(() => store.getters['general/isH160Formatted']); const tokenSymbol = computed(() => route.query.token as string); + const isEthWallet = computed(() => store.getters['general/isEthWallet']); const isLoading = computed(() => store.getters['general/isLoading']); + const substrateAccounts = computed( + () => store.getters['general/substrateAccounts'] + ); const isTransferNativeToken = computed( () => tokenSymbol.value === nativeTokenSymbol.value.toLowerCase() @@ -73,7 +92,8 @@ export function useTokenTransfer(selectedToken: Ref) { }); const isValidDestAddress = computed(() => { - const isOnlyAcceptEvmAddress = isH160.value && !isTransferNativeToken.value; + const isOnlyAcceptEvmAddress = + isH160.value && !isTransferNativeToken.value && !isSupportXvmTransfer.value; return isOnlyAcceptEvmAddress ? isValidEvmAddress(toAddress.value) : isValidAddressPolkadotAddress(toAddress.value, ASTAR_SS58_FORMAT) || @@ -94,15 +114,16 @@ export function useTokenTransfer(selectedToken: Ref) { toAddressBalance.value = 0; }; - const finalizedCallback = (hash: string): void => { - addTxHistories({ - hash: String(hash), - type: HistoryTxType.Transfer, - address: currentAccount.value, - }); + const finalizeCallback = (): void => { router.push(Path.Assets); }; + const toastInvalidAddress = () => + store.dispatch('general/showAlertMsg', { + msg: 'assets.invalidAddress', + alertType: 'error', + }); + const toMaxAmount = async (): Promise => { transferAmt.value = String(selectedToken.value.userBalance); }; @@ -112,9 +133,15 @@ export function useTokenTransfer(selectedToken: Ref) { const transferAmtRef = Number(transferAmt.value); try { if (transferAmtRef > fromAddressBalance.value) { - errMsg.value = 'warning.insufficientBalance'; + errMsg.value = t('warning.insufficientBalance', { + token: selectedToken.value.metadata.symbol, + }); } else if (toAddress.value && !isValidDestAddress.value) { errMsg.value = 'warning.inputtedInvalidDestAddress'; + } else if (!transferableBalance.value) { + errMsg.value = t('warning.insufficientBalance', { + token: nativeTokenSymbol.value, + }); } else { errMsg.value = ''; } @@ -123,6 +150,145 @@ export function useTokenTransfer(selectedToken: Ref) { } }; + const callH160NativeTransfer = async ({ + transferAmt, + fromAddress, + toAddress, + web3, + gasPrice, + symbol, + finalizeCallback, + }: { + transferAmt: number; + fromAddress: string; + toAddress: string; + web3: Web3; + gasPrice: string; + symbol: string; + finalizeCallback: () => void; + }): Promise => { + const destinationAddress = buildEvmAddress(toAddress); + if (!ethers.utils.isAddress(destinationAddress)) { + toastInvalidAddress(); + return; + } + const rawTx: TransactionConfig = { + nonce: await web3.eth.getTransactionCount(fromAddress), + gasPrice: web3.utils.toHex(gasPrice), + from: fromAddress, + to: destinationAddress, + value: web3.utils.toWei(String(transferAmt), 'ether'), + }; + + const estimatedGas = await web3.eth.estimateGas(rawTx); + + await web3.eth + .sendTransaction({ ...rawTx, gas: estimatedGas }) + .once('transactionHash', (transactionHash) => { + store.commit('general/setLoading', true); + }) + .then(({ transactionHash }) => { + store.dispatch('general/showAlertMsg', { + msg: t('toast.completedMessage', { + hash: transactionHash, + symbol, + transferAmt, + toAddress, + }), + alertType: 'success', + txHash: transactionHash, + }); + store.commit('general/setLoading', false); + addTxHistories({ + hash: transactionHash, + type: HistoryTxType.Transfer, + address: fromAddress, + }); + finalizeCallback(); + }) + .catch((error: any) => { + console.error(error); + store.commit('general/setLoading', false); + store.dispatch('general/showAlertMsg', { + msg: error.message, + alertType: 'error', + }); + }); + }; + + const callErc20Transfer = async ({ + transferAmt, + fromAddress, + toAddress, + contractAddress, + gasPrice, + web3, + symbol, + finalizeCallback, + }: { + transferAmt: string; + fromAddress: string; + toAddress: string; + contractAddress: string; + gasPrice: string; + web3: Web3; + symbol: string; + finalizeCallback: () => void; + }): Promise => { + if (!isH160.value) return; + const destAddress = isValidAddressPolkadotAddress(toAddress) + ? buildEvmAddress(toAddress) + : toAddress; + if (!ethers.utils.isAddress(destAddress)) { + toastInvalidAddress(); + return; + } + const contract = new web3.eth.Contract(ABI as AbiItem[], contractAddress); + + const rawTx: TransactionConfig = { + nonce: await web3.eth.getTransactionCount(fromAddress), + gasPrice: web3.utils.toHex(gasPrice), + from: fromAddress, + to: contractAddress, + value: '0x0', + data: contract.methods.transfer(destAddress, transferAmt).encodeABI(), + }; + + const estimatedGas = await web3.eth.estimateGas(rawTx); + await web3.eth + .sendTransaction({ ...rawTx, gas: estimatedGas }) + .once('transactionHash', (transactionHash) => { + store.commit('general/setLoading', true); + }) + .then(({ transactionHash }) => { + store.dispatch('general/showAlertMsg', { + msg: t('toast.completedMessage', { + hash: transactionHash, + symbol, + transferAmt, + toAddress, + }), + alertType: 'success', + txHash: transactionHash, + }); + store.commit('general/setLoading', false); + addTxHistories({ + hash: transactionHash, + type: HistoryTxType.Transfer, + address: fromAddress, + }); + finalizeCallback(); + }) + .catch((error: any) => { + console.error(error); + store.commit('general/setLoading', false); + store.dispatch('general/showAlertMsg', { + msg: error.message, + alertType: 'error', + }); + }); + }; + const transferAsset = async ({ transferAmt, toAddress, @@ -137,31 +303,82 @@ export function useTokenTransfer(selectedToken: Ref) { } const receivingAddress = isValidEvmAddress(toAddress) ? toSS58Address(toAddress) : toAddress; + const txResHandler = async (result: ISubmittableResult): Promise => { + const res = await handleResult(result); + finalizeCallback(); + return res; + }; + const decimals = Number(selectedToken.value.metadata.decimals); const amount = ethers.utils.parseUnits(String(transferAmt), decimals).toString(); - try { - const tokenTransferService = container.get( - Symbols.TokenTransferService - ); - if (isH160.value) { - await tokenTransferService.transferEvmAsset({ - senderAddress: currentAccount.value, - toAddress, - amount: String(transferAmt), + const callH160Transfer = async (): Promise => { + const web3 = new Web3(ethProvider.value as any); + const gasPrice = await getEvmGas(web3, selectedGas.value.price); + if (isTransferNativeToken.value) { + await callH160NativeTransfer({ + fromAddress: currentAccount.value, + toAddress: toAddress, + transferAmt, + gasPrice, + web3, + symbol, + finalizeCallback, + }); + } else { + await callErc20Transfer({ + fromAddress: currentAccount.value, + toAddress: toAddress, + transferAmt: amount, contractAddress: selectedToken.value.mappedERC20Addr, - decimals, - finalizedCallback, + gasPrice, + web3, + symbol, + finalizeCallback, + }); + } + }; + + const callSs58Transfer = async (): Promise => { + if (isTransferNativeToken.value) { + const transaction = $api!.tx.balances.transferKeepAlive(receivingAddress, amount); + await signAndSend({ + transaction, + senderAddress: currentAccount.value, + substrateAccounts: substrateAccounts.value, + isCustomSignature: isEthWallet.value, + txResHandler, + handleCustomExtrinsic, + dispatch: store.dispatch, + tip: selectedTip.value.price, + txType: HistoryTxType.Transfer, }); } else { - await tokenTransferService.transferNativeAsset({ - assetId: selectedToken.value.id, + const transaction = $api!.tx.assets.transfer( + new BN(selectedToken.value.id), + receivingAddress, + amount + ); + await signAndSend({ + transaction, senderAddress: currentAccount.value, - receivingAddress: receivingAddress, - amount, - finalizedCallback, + substrateAccounts: substrateAccounts.value, + isCustomSignature: false, + txResHandler, + handleCustomExtrinsic, + dispatch: store.dispatch, + tip: selectedTip.value.price, + txType: HistoryTxType.Transfer, }); } + }; + + try { + if (isH160.value) { + await callH160Transfer(); + } else { + await callSs58Transfer(); + } } catch (e: any) { console.error(e); store.dispatch('general/showAlertMsg', { @@ -200,9 +417,12 @@ export function useTokenTransfer(selectedToken: Ref) { if (isTransferNativeToken.value) { toAddressBalance.value = await getNativeTokenBalance(destAddress); } else if (isH160.value) { + const address = isValidAddressPolkadotAddress(toAddress.value) + ? buildEvmAddress(toAddress.value) + : toAddress.value; const balance = await getTokenBal({ srcChainId, - address: toAddress.value, + address, tokenAddress: selectedToken.value.mappedERC20Addr, tokenSymbol: selectedToken.value.metadata.symbol, });