diff --git a/package.json b/package.json index b8d1359473..4e5800d1e3 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@reach/dialog": "^0.17.0", "@reach/portal": "^0.17.0", "@reduxjs/toolkit": "1.9.3", + "@safe-global/safe-apps-sdk": "^9.1.0", "@sentry/react": "^8.34.0", "@tanstack/react-query": "^5.52.1", "@use-gesture/react": "^10.2.27", diff --git a/src/state/transactions/updater.tsx b/src/state/transactions/updater.tsx index 5ee55de7e7..de77cd9660 100644 --- a/src/state/transactions/updater.tsx +++ b/src/state/transactions/updater.tsx @@ -1,12 +1,14 @@ import { BigNumber } from '@ethersproject/bignumber' import { ChainId } from '@kyberswap/ks-sdk-core' +import SafeAppsSDK, { TransactionStatus as SafeTransactionStatus } from '@safe-global/safe-apps-sdk' import { ethers } from 'ethers' import { findReplacementTx } from 'find-replacement-tx' import { useEffect, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' import { NotificationType } from 'components/Announcement/type' -import { useActiveWeb3React } from 'hooks' +import { CONNECTION } from 'components/Web3Provider' +import { useActiveWeb3React, useWeb3React } from 'hooks' import useMixpanel, { MIXPANEL_TYPE, NEED_CHECK_SUBGRAPH_TRANSACTION_TYPES } from 'hooks/useMixpanel' import { useBlockNumber, useKyberSwapConfig, useTransactionNotify } from 'state/application/hooks' import { AppDispatch, AppState } from 'state/index' @@ -16,6 +18,8 @@ import { findTx } from 'utils' import { checkedTransaction, finalizeTransaction, modifyTransaction, removeTx, replaceTx } from './actions' import { SerializableTransactionReceipt, TRANSACTION_TYPE, TransactionDetails } from './type' +const appsSdk = new SafeAppsSDK() + function shouldCheck( lastBlockNumber: number, tx?: { addedTime: number; receipt?: SerializableTransactionReceipt; lastCheckedBlockNumber?: number }, @@ -40,6 +44,7 @@ function shouldCheck( export default function Updater(): null { const { chainId, account } = useActiveWeb3React() + const { connector } = useWeb3React() const { readProvider } = useKyberSwapConfig(chainId) const lastBlockNumber = useBlockNumber() @@ -55,6 +60,122 @@ export default function Updater(): null { const { mixpanelHandler, subgraphMixpanelHandler } = useMixpanel() const transactionNotify = useTransactionNotify() + const handleTransactionReceipt = (hash: string, receipt: any) => { + if (!lastBlockNumber) return + if ( + !receipt || + receipt?.txStatus === SafeTransactionStatus.AWAITING_EXECUTION || + receipt?.txStatus === SafeTransactionStatus.AWAITING_CONFIRMATIONS + ) { + dispatch(checkedTransaction({ chainId, hash, blockNumber: lastBlockNumber })) + return + } + + const transaction = findTx(transactions, receipt.transactionHash) + if (!transaction) return + dispatch( + finalizeTransaction({ + chainId, + hash: receipt.transactionHash, + receipt: { + blockHash: receipt.blockHash, + status: receipt.status, + }, + needCheckSubgraph: NEED_CHECK_SUBGRAPH_TRANSACTION_TYPES.includes(transaction.type), + }), + ) + + transactionNotify({ + hash: receipt.transactionHash, + type: receipt.status === 1 ? NotificationType.SUCCESS : NotificationType.ERROR, + account: account ?? '', + }) + if (receipt.status === 1) { + // Swapped (address sender, address srcToken, address dstToken, address dstReceiver, uint256 spentAmount, uint256 returnAmount) + const swapEventTopic = ethers.utils.id('Swapped(address,address,address,address,uint256,uint256)') + const swapLogs = receipt.logs.filter((log: any) => log.topics[0] === swapEventTopic) + // take the last swap event + const lastSwapEvent = swapLogs.slice(-1)[0] + + // decode the data + const swapInterface = new ethers.utils.Interface([ + 'event Swapped (address sender, address srcToken, address dstToken, address dstReceiver, uint256 spentAmount, uint256 returnAmount)', + ]) + const parsed = swapInterface.parseLog(lastSwapEvent) + + if ( + (transaction.extraInfo as any)?.tokenAmountOut && + transaction.extraInfo?.arbitrary?.outputDecimals !== undefined + ) { + const extraInfo = { ...transaction.extraInfo } + ;(extraInfo as any).tokenAmountOut = ethers.utils.formatUnits( + parsed.args.returnAmount.toString(), + transaction.extraInfo?.arbitrary?.outputDecimals, + ) + dispatch( + modifyTransaction({ + chainId: transaction.chainId, + hash: transaction.hash, + extraInfo, + }), + ) + } + + const arbitrary = transaction.extraInfo?.arbitrary + switch (transaction.type) { + case TRANSACTION_TYPE.SWAP: { + if (!arbitrary) return + if (account && arbitrary.isPermitSwap) { + dispatch(revokePermit({ chainId, address: arbitrary.inputAddress, account })) + } + mixpanelHandler(MIXPANEL_TYPE.SWAP_COMPLETED, { + arbitrary, + actual_gas: receipt.gasUsed || BigNumber.from(0), + gas_price: receipt.effectiveGasPrice || BigNumber.from(0), + tx_hash: receipt.transactionHash, + feeInfo: arbitrary.feeInfo, + }) + break + } + case TRANSACTION_TYPE.BRIDGE: { + if (arbitrary) { + mixpanelHandler(MIXPANEL_TYPE.BRIDGE_TRANSACTION_SUBMIT, { + ...arbitrary, + tx_hash: receipt.transactionHash, + }) + } + break + } + case TRANSACTION_TYPE.ELASTIC_COLLECT_FEE: { + if (arbitrary) { + mixpanelHandler(MIXPANEL_TYPE.ELASTIC_COLLECT_FEES_COMPLETED, arbitrary) + } + break + } + case TRANSACTION_TYPE.ELASTIC_INCREASE_LIQUIDITY: { + if (arbitrary) { + mixpanelHandler(MIXPANEL_TYPE.ELASTIC_INCREASE_LIQUIDITY_COMPLETED, { + ...arbitrary, + tx_hash: receipt.transactionHash, + }) + } + break + } + case TRANSACTION_TYPE.CANCEL_LIMIT_ORDER: { + if (arbitrary) { + mixpanelHandler(MIXPANEL_TYPE.LO_CANCEL_ORDER_SUBMITTED, { + ...arbitrary, + tx_hash: receipt.transactionHash, + }) + } + break + } + default: + break + } + } + } + useEffect(() => { if (!readProvider || !lastBlockNumber) return const uniqueTransactions = [ @@ -111,122 +232,38 @@ export default function Updater(): null { } }) .catch(console.warn) - readProvider - .getTransactionReceipt(hash) - .then(receipt => { - if (!receipt) { - dispatch(checkedTransaction({ chainId, hash, blockNumber: lastBlockNumber })) - return - } - const transaction = findTx(transactions, receipt.transactionHash) - if (!transaction) return - dispatch( - finalizeTransaction({ - chainId, - hash: receipt.transactionHash, - receipt: { - blockHash: receipt.blockHash, - status: receipt.status, - }, - needCheckSubgraph: NEED_CHECK_SUBGRAPH_TRANSACTION_TYPES.includes(transaction.type), - }), - ) - - transactionNotify({ - hash: receipt.transactionHash, - type: receipt.status === 1 ? NotificationType.SUCCESS : NotificationType.ERROR, - account: account ?? '', + if (connector?.id === CONNECTION.SAFE_CONNECTOR_ID) { + appsSdk.txs + .getBySafeTxHash(hash) + .then(receipt => { + handleTransactionReceipt( + hash, + receipt + ? { + ...receipt, + transactionHash: hash, + blockHash: '', + status: receipt.txStatus === SafeTransactionStatus.SUCCESS ? 1 : 0, + } + : undefined, + ) }) - if (receipt.status === 1) { - // Swapped (address sender, address srcToken, address dstToken, address dstReceiver, uint256 spentAmount, uint256 returnAmount) - const swapEventTopic = ethers.utils.id('Swapped(address,address,address,address,uint256,uint256)') - const swapLogs = receipt.logs.filter(log => log.topics[0] === swapEventTopic) - // take the last swap event - const lastSwapEvent = swapLogs.slice(-1)[0] - - // decode the data - const swapInterface = new ethers.utils.Interface([ - 'event Swapped (address sender, address srcToken, address dstToken, address dstReceiver, uint256 spentAmount, uint256 returnAmount)', - ]) - const parsed = swapInterface.parseLog(lastSwapEvent) - - if ( - (transaction.extraInfo as any)?.tokenAmountOut && - transaction.extraInfo?.arbitrary?.outputDecimals !== undefined - ) { - const extraInfo = { ...transaction.extraInfo } - ;(extraInfo as any).tokenAmountOut = ethers.utils.formatUnits( - parsed.args.returnAmount.toString(), - transaction.extraInfo?.arbitrary?.outputDecimals, - ) - dispatch( - modifyTransaction({ - chainId: transaction.chainId, - hash: transaction.hash, - extraInfo, - }), - ) - } - - const arbitrary = transaction.extraInfo?.arbitrary - switch (transaction.type) { - case TRANSACTION_TYPE.SWAP: { - if (!arbitrary) return - if (account && arbitrary.isPermitSwap) { - dispatch(revokePermit({ chainId, address: arbitrary.inputAddress, account })) - } - mixpanelHandler(MIXPANEL_TYPE.SWAP_COMPLETED, { - arbitrary, - actual_gas: receipt.gasUsed || BigNumber.from(0), - gas_price: receipt.effectiveGasPrice || BigNumber.from(0), - tx_hash: receipt.transactionHash, - feeInfo: arbitrary.feeInfo, - }) - break - } - case TRANSACTION_TYPE.BRIDGE: { - if (arbitrary) { - mixpanelHandler(MIXPANEL_TYPE.BRIDGE_TRANSACTION_SUBMIT, { - ...arbitrary, - tx_hash: receipt.transactionHash, - }) - } - break - } - case TRANSACTION_TYPE.ELASTIC_COLLECT_FEE: { - if (arbitrary) { - mixpanelHandler(MIXPANEL_TYPE.ELASTIC_COLLECT_FEES_COMPLETED, arbitrary) - } - break - } - case TRANSACTION_TYPE.ELASTIC_INCREASE_LIQUIDITY: { - if (arbitrary) { - mixpanelHandler(MIXPANEL_TYPE.ELASTIC_INCREASE_LIQUIDITY_COMPLETED, { - ...arbitrary, - tx_hash: receipt.transactionHash, - }) - } - break - } - case TRANSACTION_TYPE.CANCEL_LIMIT_ORDER: { - if (arbitrary) { - mixpanelHandler(MIXPANEL_TYPE.LO_CANCEL_ORDER_SUBMITTED, { - ...arbitrary, - tx_hash: receipt.transactionHash, - }) - } - break - } - default: - break - } - } - }) - .catch((error: any) => { - console.error(`failed to check transaction hash: ${hash}`, error) - }) + .catch((error: any) => { + console.error(`failed to check transaction hash: ${hash}`, error) + }) + } else { + readProvider + .getTransactionReceipt(hash) + .then(receipt => { + handleTransactionReceipt(hash, receipt) + }) + .catch((error: any) => { + console.error(`failed to check transaction hash: ${hash}`, error) + }) + } }) + uniqueTransactions .filter(hash => findTx(transactions, hash)?.extraInfo?.needCheckSubgraph) .forEach(async (hash: string) => {