diff --git a/src/ui/utils/sendTransaction.ts b/src/ui/utils/sendTransaction.ts new file mode 100644 index 00000000000..331d75d165f --- /dev/null +++ b/src/ui/utils/sendTransaction.ts @@ -0,0 +1,329 @@ +import { + CHAINS_ENUM, + EVENTS, + INTERNAL_REQUEST_ORIGIN, + INTERNAL_REQUEST_SESSION, +} from '@/constant'; +import { intToHex, WalletControllerType } from '@/ui/utils'; +import { findChain } from '@/utils/chain'; +import { + calcGasLimit, + calcMaxPriorityFee, + checkGasAndNonce, + explainGas, + getNativeTokenBalance, + getPendingTxs, +} from '@/utils/transaction'; +import { Tx } from '@rabby-wallet/rabby-api/dist/types'; +import BigNumber from 'bignumber.js'; +import { + fetchActionRequiredData, + parseAction, +} from '@/ui/views/Approval/components/Actions/utils'; +import Browser from 'webextension-polyfill'; +import eventBus from '@/eventBus'; + +// fail code +export enum FailedCode { + GasNotEnough = 'GasNotEnough', + GasTooHigh = 'GasTooHigh', + SubmitTxFailed = 'SubmitTxFailed', + DefaultFailed = 'DefaultFailed', +} + +type ProgressStatus = 'building' | 'builded' | 'signed' | 'submitted'; + +/** + * send transaction without rpcFlow + */ +export const sendTransaction = async ({ + tx, + chainServerId, + wallet, + ignoreGasCheck, + onProgress, +}: { + tx: Tx; + chainServerId: string; + wallet: WalletControllerType; + ignoreGasCheck?: boolean; + onProgress?: (status: ProgressStatus) => void; +}) => { + onProgress?.('building'); + const chain = findChain({ + serverId: chainServerId, + })!; + const support1559 = chain.eip['1559']; + const { address } = (await wallet.getCurrentAccount())!; + const recommendNonce = await wallet.getRecommendNonce({ + from: tx.from, + chainId: chain.id, + }); + + // get gas + const gasMarket = await wallet.openapi.gasMarket(chainServerId); + const normalGas = gasMarket.find((item) => item.level === 'normal')!; + const signingTxId = await wallet.addSigningTx(tx); + + // pre exec tx + const preExecResult = await wallet.openapi.preExecTx({ + tx: { + ...tx, + nonce: recommendNonce, + data: tx.data, + value: tx.value || '0x0', + gasPrice: intToHex(Math.round(normalGas.price)), + }, + origin: INTERNAL_REQUEST_ORIGIN, + address: address, + updateNonce: true, + pending_tx_list: await getPendingTxs({ + recommendNonce, + wallet, + address, + }), + }); + + const balance = await getNativeTokenBalance({ + wallet, + chainId: chain.id, + address, + }); + let estimateGas = 0; + if (preExecResult.gas.success) { + estimateGas = preExecResult.gas.gas_limit || preExecResult.gas.gas_used; + } + const { gas: gasRaw, needRatio, gasUsed } = await wallet.getRecommendGas({ + gasUsed: preExecResult.gas.gas_used, + gas: estimateGas, + tx, + chainId: chain.id, + }); + const gas = new BigNumber(gasRaw); + const { gasLimit, recommendGasLimitRatio } = await calcGasLimit({ + chain, + tx, + gas, + selectedGas: normalGas, + nativeTokenBalance: balance, + explainTx: preExecResult, + needRatio, + wallet, + }); + + // calc gasCost + const gasCost = await explainGas({ + gasUsed, + gasPrice: normalGas.price, + chainId: chain.id, + nativeTokenPrice: preExecResult.native_token.price, + wallet, + tx, + gasLimit, + }); + + // check gas errors + const checkErrors = checkGasAndNonce({ + recommendGasLimit: `0x${gas.toString(16)}`, + recommendNonce, + gasLimit: Number(gasLimit), + nonce: Number(recommendNonce || tx.nonce), + gasExplainResponse: gasCost, + isSpeedUp: false, + isCancel: false, + tx, + isGnosisAccount: false, + nativeTokenBalance: balance, + recommendGasLimitRatio, + }); + + const isGasNotEnough = checkErrors.some((e) => e.code === 3001); + const ETH_GAS_USD_LIMIT = process.env.DEBUG + ? (await Browser.storage.local.get('DEBUG_ETH_GAS_USD_LIMIT')) + .DEBUG_ETH_GAS_USD_LIMIT || 20 + : 20; + const OTHER_CHAIN_GAS_USD_LIMIT = process.env.DEBUG + ? (await Browser.storage.local.get('DEBUG_OTHER_CHAIN_GAS_USD_LIMIT')) + .DEBUG_OTHER_CHAIN_GAS_USD_LIMIT || 5 + : 5; + let failedCode; + if (isGasNotEnough) { + failedCode = FailedCode.GasNotEnough; + } else if ( + !ignoreGasCheck && + // eth gas > $20 + ((chain.enum === CHAINS_ENUM.ETH && + gasCost.gasCostUsd.isGreaterThan(ETH_GAS_USD_LIMIT)) || + // other chain gas > $5 + (chain.enum !== CHAINS_ENUM.ETH && + gasCost.gasCostUsd.isGreaterThan(OTHER_CHAIN_GAS_USD_LIMIT))) + ) { + failedCode = FailedCode.GasTooHigh; + } + + if (failedCode) { + throw { + name: failedCode, + gasCost, + }; + } + + // generate tx with gas + const transaction: Tx = { + from: tx.from, + to: tx.to, + data: tx.data, + nonce: recommendNonce, + value: tx.value, + chainId: tx.chainId, + gas: gasLimit, + }; + const maxPriorityFee = calcMaxPriorityFee([], normalGas, chain.id, true); + const maxFeePerGas = intToHex(Math.round(normalGas.price)); + + if (support1559) { + transaction.maxFeePerGas = maxFeePerGas; + transaction.maxPriorityFeePerGas = + maxPriorityFee <= 0 + ? tx.maxFeePerGas + : intToHex(Math.round(maxPriorityFee)); + } else { + (transaction as Tx).gasPrice = maxFeePerGas; + } + + // fetch action data + const actionData = await wallet.openapi.parseTx({ + chainId: chain.serverId, + tx: { + ...tx, + gas: '0x0', + nonce: recommendNonce || '0x1', + value: tx.value || '0x0', + to: tx.to || '', + }, + origin: origin || '', + addr: address, + }); + const parsed = parseAction( + actionData.action, + preExecResult.balance_change, + { + ...tx, + gas: '0x0', + nonce: recommendNonce || '0x1', + value: tx.value || '0x0', + }, + preExecResult.pre_exec_version, + preExecResult.gas.gas_used + ); + const requiredData = await fetchActionRequiredData({ + actionData: parsed, + contractCall: actionData.contract_call, + chainId: chain.serverId, + address, + wallet, + tx: { + ...tx, + gas: '0x0', + nonce: recommendNonce || '0x1', + value: tx.value || '0x0', + }, + origin, + }); + + await wallet.updateSigningTx(signingTxId, { + rawTx: { + nonce: recommendNonce, + }, + explain: { + ...preExecResult, + }, + action: { + actionData: parsed, + requiredData, + }, + }); + const logId = actionData.log_id; + const estimateGasCost = { + gasCostUsd: gasCost.gasCostUsd, + gasCostAmount: gasCost.gasCostAmount, + nativeTokenSymbol: preExecResult.native_token.symbol, + gasPrice: normalGas.price, + nativeTokenPrice: preExecResult.native_token.price, + }; + + onProgress?.('builded'); + + if (process.env.DEBUG) { + const { DEBUG_MOCK_SUBMIT } = await Browser.storage.local.get( + 'DEBUG_MOCK_SUBMIT' + ); + + if (DEBUG_MOCK_SUBMIT) { + return { + txHash: 'mock_hash', + gasCost: estimateGasCost, + }; + } + } + + // submit tx + let hash = ''; + try { + hash = await Promise.race([ + wallet.ethSendTransaction({ + data: { + $ctx: {}, + params: [transaction], + }, + session: INTERNAL_REQUEST_SESSION, + approvalRes: { + ...transaction, + signingTxId, + logId: logId, + }, + pushed: false, + result: undefined, + }), + new Promise((_, reject) => { + eventBus.once(EVENTS.LEDGER.REJECTED, async (data) => { + reject(new Error(data)); + }); + }), + ]); + } catch (e) { + const err = new Error(e.message); + err.name = FailedCode.SubmitTxFailed; + throw err; + } + + onProgress?.('signed'); + + // wait tx completed + const txCompleted = await new Promise<{ gasUsed: number }>((resolve) => { + const handler = (res) => { + if (res?.hash === hash) { + eventBus.removeEventListener(EVENTS.TX_COMPLETED, handler); + resolve(res || {}); + } + }; + eventBus.addEventListener(EVENTS.TX_COMPLETED, handler); + }); + + // calc gas cost + const gasCostAmount = new BigNumber(txCompleted.gasUsed) + .times(estimateGasCost.gasPrice) + .div(1e18); + const gasCostUsd = new BigNumber(gasCostAmount).times( + estimateGasCost.nativeTokenPrice + ); + + return { + txHash: hash, + gasCost: { + ...estimateGasCost, + gasCostUsd, + gasCostAmount, + }, + }; +}; diff --git a/src/ui/views/ApprovalManagePage/components/BatchRevoke/RevokeTable.tsx b/src/ui/views/ApprovalManagePage/components/BatchRevoke/RevokeTable.tsx index efa9dd8bdb6..5b512092e0c 100644 --- a/src/ui/views/ApprovalManagePage/components/BatchRevoke/RevokeTable.tsx +++ b/src/ui/views/ApprovalManagePage/components/BatchRevoke/RevokeTable.tsx @@ -102,8 +102,11 @@ export const RevokeTable: React.FC = ({ { title: '#', key: 'index', + className: 'index-cell', render: (text, record, index) => ( - {index + 1} + + {index + 1} + ), width: 40, }, diff --git a/src/ui/views/ApprovalManagePage/components/BatchRevoke/style.less b/src/ui/views/ApprovalManagePage/components/BatchRevoke/style.less index f91de944922..3124075745f 100644 --- a/src/ui/views/ApprovalManagePage/components/BatchRevoke/style.less +++ b/src/ui/views/ApprovalManagePage/components/BatchRevoke/style.less @@ -8,6 +8,7 @@ background-color: transparent; padding: 0; margin-bottom: 0; + overflow: visible; } .ant-modal-confirm-btns { @@ -87,9 +88,16 @@ .is-last-cell { justify-content: end; - .am-virtual-table-cell-inner { - padding-right: 0; + .am-virtual-table-cell-inner { + padding-right: 0; + } } + + + .index-cell { + .am-virtual-table-cell-inner { + padding-right: 6px; + } } .status-cell { diff --git a/src/ui/views/ApprovalManagePage/components/BatchRevoke/useBatchRevokeTask.ts b/src/ui/views/ApprovalManagePage/components/BatchRevoke/useBatchRevokeTask.ts index 7b19a51743a..512d09dc25e 100644 --- a/src/ui/views/ApprovalManagePage/components/BatchRevoke/useBatchRevokeTask.ts +++ b/src/ui/views/ApprovalManagePage/components/BatchRevoke/useBatchRevokeTask.ts @@ -1,38 +1,18 @@ -import { - CHAINS_ENUM, - EVENTS, - INTERNAL_REQUEST_ORIGIN, - INTERNAL_REQUEST_SESSION, -} from '@/constant'; -import { intToHex, useWallet, WalletControllerType } from '@/ui/utils'; +import { useWallet, WalletControllerType } from '@/ui/utils'; import { ApprovalSpenderItemToBeRevoked } from '@/utils-isomorphic/approve'; import { AssetApprovalSpender } from '@/utils/approval'; -import { findChain } from '@/utils/chain'; -import { - calcGasLimit, - calcMaxPriorityFee, - checkGasAndNonce, - explainGas, - getNativeTokenBalance, - getPendingTxs, -} from '@/utils/transaction'; import { Tx } from '@rabby-wallet/rabby-api/dist/types'; import BigNumber from 'bignumber.js'; import PQueue from 'p-queue'; import React from 'react'; import { findIndexRevokeList } from '../../utils'; -import { - fetchActionRequiredData, - parseAction, -} from '@/ui/views/Approval/components/Actions/utils'; -import eventBus from '@/eventBus'; import i18n from '@/i18n'; -import Browser from 'webextension-polyfill'; +import { FailedCode, sendTransaction } from '@/ui/utils/sendTransaction'; +export { FailedCode } from '@/ui/utils/sendTransaction'; async function buildTx( wallet: WalletControllerType, - item: ApprovalSpenderItemToBeRevoked, - ignoreGasCheck = false + item: ApprovalSpenderItemToBeRevoked ) { // generate tx let tx: Tx; @@ -73,214 +53,7 @@ async function buildTx( tx = data.params[0]; } - const chain = findChain({ - serverId: item.chainServerId, - })!; - const support1559 = chain.eip['1559']; - const { address } = (await wallet.getCurrentAccount())!; - const recommendNonce = await wallet.getRecommendNonce({ - from: tx.from, - chainId: chain.id, - }); - - // get gas - const gasMarket = await wallet.openapi.gasMarket(item.chainServerId); - const normalGas = gasMarket.find((item) => item.level === 'normal')!; - const signingTxId = await wallet.addSigningTx(tx); - - // pre exec tx - const preExecResult = await wallet.openapi.preExecTx({ - tx: { - ...tx, - nonce: recommendNonce, - data: tx.data, - value: tx.value || '0x0', - gasPrice: intToHex(Math.round(normalGas.price)), - }, - origin: INTERNAL_REQUEST_ORIGIN, - address: address, - updateNonce: true, - pending_tx_list: await getPendingTxs({ - recommendNonce, - wallet, - address, - }), - }); - - const balance = await getNativeTokenBalance({ - wallet, - chainId: chain.id, - address, - }); - let estimateGas = 0; - if (preExecResult.gas.success) { - estimateGas = preExecResult.gas.gas_limit || preExecResult.gas.gas_used; - } - const { gas: gasRaw, needRatio, gasUsed } = await wallet.getRecommendGas({ - gasUsed: preExecResult.gas.gas_used, - gas: estimateGas, - tx, - chainId: chain.id, - }); - const gas = new BigNumber(gasRaw); - const { gasLimit, recommendGasLimitRatio } = await calcGasLimit({ - chain, - tx, - gas, - selectedGas: normalGas, - nativeTokenBalance: balance, - explainTx: preExecResult, - needRatio, - wallet, - }); - - // calc gasCost - const gasCost = await explainGas({ - gasUsed, - gasPrice: normalGas.price, - chainId: chain.id, - nativeTokenPrice: preExecResult.native_token.price, - wallet, - tx, - gasLimit, - }); - - // check gas errors - const checkErrors = checkGasAndNonce({ - recommendGasLimit: `0x${gas.toString(16)}`, - recommendNonce, - gasLimit: Number(gasLimit), - nonce: Number(recommendNonce || tx.nonce), - gasExplainResponse: gasCost, - isSpeedUp: false, - isCancel: false, - tx, - isGnosisAccount: false, - nativeTokenBalance: balance, - recommendGasLimitRatio, - }); - - const isGasNotEnough = checkErrors.some((e) => e.code === 3001); - const ETH_GAS_USD_LIMIT = process.env.DEBUG - ? (await Browser.storage.local.get('DEBUG_ETH_GAS_USD_LIMIT')) - .DEBUG_ETH_GAS_USD_LIMIT || 20 - : 20; - const OTHER_CHAIN_GAS_USD_LIMIT = process.env.DEBUG - ? (await Browser.storage.local.get('DEBUG_OTHER_CHAIN_GAS_USD_LIMIT')) - .DEBUG_OTHER_CHAIN_GAS_USD_LIMIT || 5 - : 5; - let failedCode = 0; - if (isGasNotEnough) { - failedCode = FailedCode.GasNotEnough; - } else if ( - !ignoreGasCheck && - // eth gas > $20 - ((chain.enum === CHAINS_ENUM.ETH && - gasCost.gasCostUsd.isGreaterThan(ETH_GAS_USD_LIMIT)) || - // other chain gas > $5 - (chain.enum !== CHAINS_ENUM.ETH && - gasCost.gasCostUsd.isGreaterThan(OTHER_CHAIN_GAS_USD_LIMIT))) - ) { - failedCode = FailedCode.GasTooHigh; - } - - // generate tx with gas - const transaction: Tx = { - from: tx.from, - to: tx.to, - data: tx.data, - nonce: recommendNonce, - value: tx.value, - chainId: tx.chainId, - gas: gasLimit, - }; - const maxPriorityFee = calcMaxPriorityFee([], normalGas, chain.id, true); - const maxFeePerGas = intToHex(Math.round(normalGas.price)); - - if (support1559) { - transaction.maxFeePerGas = maxFeePerGas; - transaction.maxPriorityFeePerGas = - maxPriorityFee <= 0 - ? tx.maxFeePerGas - : intToHex(Math.round(maxPriorityFee)); - } else { - (transaction as Tx).gasPrice = maxFeePerGas; - } - - // fetch action data - const actionData = await wallet.openapi.parseTx({ - chainId: chain.serverId, - tx: { - ...tx, - gas: '0x0', - nonce: recommendNonce || '0x1', - value: tx.value || '0x0', - to: tx.to || '', - }, - origin: origin || '', - addr: address, - }); - const parsed = parseAction( - actionData.action, - preExecResult.balance_change, - { - ...tx, - gas: '0x0', - nonce: recommendNonce || '0x1', - value: tx.value || '0x0', - }, - preExecResult.pre_exec_version, - preExecResult.gas.gas_used - ); - const requiredData = await fetchActionRequiredData({ - actionData: parsed, - contractCall: actionData.contract_call, - chainId: chain.serverId, - address, - wallet, - tx: { - ...tx, - gas: '0x0', - nonce: recommendNonce || '0x1', - value: tx.value || '0x0', - }, - origin, - }); - - await wallet.updateSigningTx(signingTxId, { - rawTx: { - nonce: recommendNonce, - }, - explain: { - ...preExecResult, - }, - action: { - actionData: parsed, - requiredData, - }, - }); - - return { - transaction, - signingTxId, - logId: actionData.log_id, - failedCode, - estimateGasCost: { - gasCostUsd: gasCost.gasCostUsd, - gasCostAmount: gasCost.gasCostAmount, - nativeTokenSymbol: preExecResult.native_token.symbol, - gasPrice: normalGas.price, - nativeTokenPrice: preExecResult.native_token.price, - }, - }; -} - -// fail code -export enum FailedCode { - GasNotEnough = 1, - GasTooHigh = 2, - SubmitTxFailed = 3, - DefaultFailed = 4, + return tx; } export const FailReason = { @@ -399,108 +172,30 @@ export const useBatchRevokeTask = () => { setList((prev) => updateAssetApprovalSpender(prev, cloneItem)); try { - // build tx - const { - transaction, - signingTxId, - logId, - estimateGasCost, - failedCode, - } = await buildTx(wallet, revokeItem, ignoreGasCheck); - - if (failedCode) { - cloneItem.$status = { - status: 'fail', - failedCode, - gasCost: estimateGasCost, - }; - return; - } - - if (process.env.DEBUG) { - const { DEBUG_MOCK_SUBMIT } = await Browser.storage.local.get( - 'DEBUG_MOCK_SUBMIT' - ); - - if (DEBUG_MOCK_SUBMIT) { - cloneItem.$status = { - status: 'success', - txHash: 'mock_hash', - gasCost: estimateGasCost, - }; - return; - } - } - - // submit tx - let hash = ''; - setTxStatus('sended'); - - try { - hash = await Promise.race([ - wallet.ethSendTransaction({ - data: { - $ctx: {}, - params: [transaction], - }, - session: INTERNAL_REQUEST_SESSION, - approvalRes: { - ...transaction, - signingTxId, - logId: logId, - }, - pushed: false, - result: undefined, - }), - new Promise((_, reject) => { - eventBus.once(EVENTS.LEDGER.REJECTED, async (data) => { - reject(new Error(data)); - }); - }), - ]); - } catch (e) { - const err = new Error(e.message); - err.name = 'SubmitTxFailed'; - throw err; - } - - setTxStatus('signed'); - - // wait tx completed - const { gasUsed } = await new Promise<{ gasUsed: number }>( - (resolve) => { - const handler = (res) => { - if (res?.hash === hash) { - eventBus.removeEventListener(EVENTS.TX_COMPLETED, handler); - resolve(res || {}); - } - }; - eventBus.addEventListener(EVENTS.TX_COMPLETED, handler); - } - ); - - // calc gas cost - const gasCostAmount = new BigNumber(gasUsed) - .times(estimateGasCost.gasPrice) - .div(1e18); - const gasCostUsd = new BigNumber(gasCostAmount).times( - estimateGasCost.nativeTokenPrice - ); - + const tx = await buildTx(wallet, revokeItem); + const result = await sendTransaction({ + tx, + ignoreGasCheck, + wallet, + chainServerId: revokeItem.chainServerId, + onProgress: (status) => { + if (status === 'builded') { + setTxStatus('sended'); + } else if (status === 'signed') { + setTxStatus('signed'); + } + }, + }); // update status cloneItem.$status = { status: 'success', - txHash: hash, - gasCost: { - ...estimateGasCost, - gasCostUsd, - gasCostAmount, - }, + txHash: result.txHash, + gasCost: result.gasCost, }; } catch (e) { let failedCode = FailedCode.DefaultFailed; - if (e.name === 'SubmitTxFailed') { - failedCode = FailedCode.SubmitTxFailed; + if (FailedCode[e.name]) { + failedCode = e.name; } console.error(e); @@ -508,6 +203,7 @@ export const useBatchRevokeTask = () => { status: 'fail', failedCode: failedCode, failedReason: e.message, + gasCost: e.gasCost, }; } finally { setList((prev) => updateAssetApprovalSpender(prev, cloneItem)); diff --git a/src/ui/views/ApprovalManagePage/index.tsx b/src/ui/views/ApprovalManagePage/index.tsx index 664d88d87c3..d1f5f3a80cb 100644 --- a/src/ui/views/ApprovalManagePage/index.tsx +++ b/src/ui/views/ApprovalManagePage/index.tsx @@ -55,8 +55,6 @@ import { openScanLinkFromChainItem, encodeRevokeItem, decodeRevokeItem, - isSelectedAllContract, - isSelectedAllAssetApprovals, TableSelectResult, } from './utils'; import { IconWithChain } from '@/ui/component/TokenWithChain'; @@ -718,7 +716,7 @@ function getColumnsForAsset({ const spendValues = getSpenderApprovalAmount(spender); return ( -
+
- + {spendValues.displayAmountText} diff --git a/src/ui/views/ApprovalManagePage/style.less b/src/ui/views/ApprovalManagePage/style.less index 441d11e3ac0..fe512d8b6e1 100644 --- a/src/ui/views/ApprovalManagePage/style.less +++ b/src/ui/views/ApprovalManagePage/style.less @@ -130,13 +130,13 @@ background-color: var(--r-neutral-card1); border-radius: @radius-value; overflow: hidden; - border: 0.5px solid var(--r-neutral-line); + border: 1px solid var(--r-neutral-line); transition: ease-in 0.2s border-color; .ant-input-affix-wrapper.search-input, .ant-input { background-color: transparent; color: var(--r-neutral-title1); - &::placeholder-shown { + &::placeholder { color: var(--r-neutral-foot); } } diff --git a/src/ui/views/ApprovalManagePage/utils.ts b/src/ui/views/ApprovalManagePage/utils.ts index 6e3df715a38..7153a0e9412 100644 --- a/src/ui/views/ApprovalManagePage/utils.ts +++ b/src/ui/views/ApprovalManagePage/utils.ts @@ -401,7 +401,7 @@ export function isSelectedAllContract( ) { const set = new Set(selectedRows.map((x) => encodeRevokeItem(x))); const result: TableSelectResult = { - isSelectedAll: true, + isSelectedAll: false, isIndeterminate: false, }; @@ -422,7 +422,8 @@ export function isSelectedAllContract( if (noSelectAll) { result.isIndeterminate = hasSelected; result.isSelectedAll = false; - break; + } else { + result.isSelectedAll = true; } } @@ -435,7 +436,7 @@ export function isSelectedAllAssetApprovals( ) { const set = new Set(selectedRows.map((x) => encodeRevokeItem(x))); const result: TableSelectResult = { - isSelectedAll: true, + isSelectedAll: false, isIndeterminate: false, }; @@ -453,7 +454,8 @@ export function isSelectedAllAssetApprovals( if (!set.has(encodeRevokeItem(revokeItem))) { result.isIndeterminate = hasSelected; result.isSelectedAll = false; - break; + } else { + result.isSelectedAll = true; } }