From ad92625a8e1254bbbf1fdeaf700cb46e31081676 Mon Sep 17 00:00:00 2001 From: sebipap Date: Mon, 27 Nov 2023 16:46:30 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=88=20get=20exa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/GetEXA/ChainSelector.tsx | 9 ++- components/GetEXA/ModalWrapper.tsx | 8 +++ components/GetEXA/ReviewRoute.tsx | 22 ++++++- components/GetEXA/Route.tsx | 23 ++++++-- components/GetEXA/SelectRoute.tsx | 91 ++++++++++++++++++++++++----- components/GetEXA/TXStatus.tsx | 8 +++ components/GetEXA/index.tsx | 17 +++++- contexts/GetEXAContext.tsx | 80 +++++++++++++++++++++++-- utils/segment.ts | 2 +- 9 files changed, 230 insertions(+), 30 deletions(-) diff --git a/components/GetEXA/ChainSelector.tsx b/components/GetEXA/ChainSelector.tsx index b7ce188fe..cbfd1fced 100644 --- a/components/GetEXA/ChainSelector.tsx +++ b/components/GetEXA/ChainSelector.tsx @@ -6,6 +6,7 @@ import DropdownMenu from 'components/DropdownMenu'; import { useGetEXA } from 'contexts/GetEXAContext'; import Image from 'next/image'; import { Chain } from 'types/Bridge'; +import { track } from 'utils/segment'; type AssetOptionProps = { chain?: Chain; option?: boolean; @@ -57,8 +58,14 @@ const ChainSelector = ({ disabled }: { disabled?: boolean }) => { (value: string) => { const c = chains?.find(({ name }) => value === name); if (c) onChainChange(c); + track('Option Selected', { + location: 'Get EXA', + name: 'chain', + value, + prevValue: chain?.name, + }); }, - [chains, onChainChange], + [chain, chains, onChainChange], ); if (!chains) return ; diff --git a/components/GetEXA/ModalWrapper.tsx b/components/GetEXA/ModalWrapper.tsx index c48c708e9..413d3894d 100644 --- a/components/GetEXA/ModalWrapper.tsx +++ b/components/GetEXA/ModalWrapper.tsx @@ -8,6 +8,7 @@ import { Box, Drawer, IconButton, Typography } from '@mui/material'; import GetEXA from '.'; import { useModal } from '../../contexts/ModalContext'; import { GetEXAProvider } from 'contexts/GetEXAContext'; +import { track } from 'utils/segment'; export default function ModalWrapper() { const { isOpen, close } = useModal('get-exa'); @@ -46,6 +47,13 @@ export default function ModalWrapper() { style={{ textDecoration: 'underline', }} + onClick={() => + track('Button Clicked', { + location: 'Get EXA ', + name: 'governance', + href: '/governance', + }) + } > {t('Governance')} diff --git a/components/GetEXA/ReviewRoute.tsx b/components/GetEXA/ReviewRoute.tsx index 0c0806150..37726a2da 100644 --- a/components/GetEXA/ReviewRoute.tsx +++ b/components/GetEXA/ReviewRoute.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { Box, Typography, Table, TableBody, TableRow, TableCell, Avatar, useTheme, Skeleton } from '@mui/material'; import { optimism } from 'wagmi/chains'; @@ -12,6 +12,7 @@ import { formatEther } from 'viem'; import ModalAlert from 'components/common/modal/ModalAlert'; import { ArrowForward } from '@mui/icons-material'; import { Protocol, Route } from '../../types/Bridge'; +import { track } from 'utils/segment'; export const RouteTable = ({ route, @@ -124,6 +125,21 @@ const ReviewRoute = () => { const { t } = useTranslation(); const { chain, asset, route, qtyOut, qtyOutUSD, qtyIn, txStep, protocol, txError, approve, socketSubmit } = useGetEXA(); + const handleConfirmClick = useCallback(() => { + socketSubmit(); + track('Button Clicked', { + location: 'Get EXA', + name: 'confirm', + }); + }, [socketSubmit]); + + const handleApproveClick = useCallback(() => { + approve(); + track('Button Clicked', { + location: 'Get EXA', + name: 'approve', + }); + }, [approve]); if (!asset?.logoURI || !chain || !qtyOut || !protocol) { return; @@ -200,7 +216,7 @@ const ReviewRoute = () => { {txStep === TXStep.CONFIRM || txStep === TXStep.CONFIRM_PENDING ? ( @@ -209,7 +225,7 @@ const ReviewRoute = () => { ) : ( { const isSelected = selectedRoute?.routeId === routeId; + const handleRouteClick = useCallback(() => { + setRoute(route); + track('Option Selected', { + location: 'Get EXA', + name: 'route', + value: protocol?.displayName || 'no option', + prevValue: + ( + selectedRoute?.userTxs?.[selectedRoute?.userTxs.length - 1] || + selectedRoute?.userTxs?.[0].steps?.[(selectedRoute?.userTxs[0]?.stepCount || 0) - 1] + )?.protocol?.displayName || 'no option', + }); + }, [protocol?.displayName, route, selectedRoute, setRoute]); + return ( { borderColor={isSelected ? 'ActiveBorder' : 'grey.200'} padding={2} borderRadius={1} - onClick={() => setRoute(route)} + onClick={handleRouteClick} sx={{ cursor: 'pointer' }} > { const { @@ -46,11 +48,74 @@ const SelectRoute = () => { const insufficientBalance = Boolean(asset && qtyIn && Number(qtyIn) > asset.amount); const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); - const handleSubmit = useCallback(() => { - if (nativeSwap) return submit(); - setScreen(Screen.REVIEW_ROUTE); - }, [nativeSwap, setScreen, submit]); + const handleSubmit = useCallback( + ({ currentTarget }: MouseEvent) => { + track('Button Clicked', { + location: 'Get EXA', + name: 'submit', + text: currentTarget.innerText, + symbol: asset?.symbol, + }); + if (nativeSwap) return submit(); + setScreen(Screen.REVIEW_ROUTE); + }, + [asset, nativeSwap, setScreen, submit], + ); + + const handleAssetChange = useCallback( + (a: AssetBalance) => { + setAsset(a); + track('Option Selected', { + location: 'Get EXA', + name: 'asset', + value: a.symbol, + prevValue: asset?.symbol, + }); + }, + [asset, setAsset], + ); + + const handleBalanceClick = useCallback(() => { + setQtyIn(String(asset?.amount)); + track('Button Clicked', { + location: 'Get EXA', + name: 'from balance', + value: asset && formatNumber(asset.amount, asset.symbol), + }); + }, [asset, setQtyIn]); + + const handleAmountBlur = useCallback( + () => track('Input Unfocused', { location: 'Get EXA', name: 'amount in', value: qtyIn }), + [qtyIn], + ); + const handleSwitchNetworkClick = useCallback(() => { + switchNetwork?.(chain?.chainId); + track('Button Clicked', { + location: 'Get EXA', + name: 'switch network', + value: chain?.name, + }); + }, [chain?.chainId, chain?.name, switchNetwork]); + const handleConnectWallet = useCallback(() => { + connect(); + track('Button Clicked', { + location: 'Get EXA', + name: 'connect wallet', + }); + }, [connect]); + + const handleEditClick = useCallback(() => { + track('Option Selected', { + name: 'edit route', + value: !route, + prevValue: !!route, + location: 'Get EXA', + }); + + if (route) setRoute(null); + if (routes) setRoute(routes[0]); + }, [route, routes, setRoute]); return ( <> @@ -73,7 +138,7 @@ const SelectRoute = () => { {assets && asset ? ( @@ -94,6 +159,7 @@ const SelectRoute = () => { inputProps={{ 'data-testid': 'get-exa-input', }} + onBlur={handleAmountBlur} /> {asset ? ( { color="grey.400" fontWeight={400} whiteSpace="nowrap" - onClick={() => setQtyIn(String(asset?.amount))} + onClick={handleBalanceClick} sx={{ '&:hover': { textDecoration: 'underline', @@ -180,7 +246,7 @@ const SelectRoute = () => { {t('Route')} {routes && routes.length > 1 && ( - )} @@ -202,12 +268,7 @@ const SelectRoute = () => { {isConnected ? ( walletChain?.id !== chain?.chainId ? ( - switchNetwork?.(chain?.chainId)} - variant="contained" - loading={switchIsLoading} - > + {t('Please switch to {{network}} network', { network: chain?.name })} ) : ( @@ -224,7 +285,7 @@ const SelectRoute = () => { ) ) : ( - )} diff --git a/components/GetEXA/TXStatus.tsx b/components/GetEXA/TXStatus.tsx index 76d473f79..282f0e802 100644 --- a/components/GetEXA/TXStatus.tsx +++ b/components/GetEXA/TXStatus.tsx @@ -8,6 +8,7 @@ import { Box, Button, CircularProgress, Typography } from '@mui/material'; import { CircularProgressWithIcon } from 'components/OperationsModal/ModalGif'; import { useGetEXA } from 'contexts/GetEXAContext'; import { Hash } from 'viem'; +import { track } from 'utils/segment'; const SpinnerThing = ({ status, @@ -80,6 +81,13 @@ const SpinnerThing = ({ }} target="_blank" href={url} + onClick={() => + track('Button Clicked', { + href: url, + location: 'Get EXA', + name: 'view tx', + }) + } disabled={!hash} > {t('View TX')} diff --git a/components/GetEXA/index.tsx b/components/GetEXA/index.tsx index b563c6c87..ec0c94c7b 100644 --- a/components/GetEXA/index.tsx +++ b/components/GetEXA/index.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { Box, IconButton } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; @@ -7,9 +7,22 @@ import ReviewRoute from './ReviewRoute'; import SelectRoute from './SelectRoute'; import { useGetEXA, Screen } from 'contexts/GetEXAContext'; import TXStatus from './TXStatus'; +import { track } from 'utils/segment'; const GetEXA = () => { const { screen, setScreen } = useGetEXA(); + const handleClose = useCallback(() => { + setScreen(Screen.SELECT_ROUTE); + track('Button Clicked', { + location: 'Get EXA', + name: 'close', + icon: 'Close', + }); + track('Modal Closed', { + name: 'Get EXA', + }); + }, [setScreen]); + return ( { {screen !== Screen.SELECT_ROUTE && ( setScreen(Screen.SELECT_ROUTE)} + onClick={handleClose} sx={{ padding: 0, ml: 'auto', diff --git a/contexts/GetEXAContext.tsx b/contexts/GetEXAContext.tsx index 55b1eb976..e653b3aca 100644 --- a/contexts/GetEXAContext.tsx +++ b/contexts/GetEXAContext.tsx @@ -19,6 +19,7 @@ import { hexToBigInt, keccak256, encodeAbiParameters, + formatUnits, } from 'viem'; import * as wagmiChains from 'wagmi/chains'; @@ -62,6 +63,7 @@ import waitForTransaction from 'utils/waitForTransaction'; import dayjs from 'dayjs'; import { splitSignature } from '@ethersproject/bytes'; import useDelayedEffect from 'hooks/useDelayedEffect'; +import { track } from 'utils/segment'; const DESTINATION_CHAIN = optimism.id; @@ -215,7 +217,22 @@ export const GetEXAProvider: FC = ({ children }) => { ...opts, gasLimit: gasLimit(gas), }); - await waitForTransaction({ hash }); + track('TX Signed', { + contractName: 'ERC20', + method: 'approve', + amount: qtyIn, + hash, + }); + const { status } = await waitForTransaction({ hash }); + + track('TX Completed', { + contractName: 'ERC20', + method: 'approve', + amount: qtyIn, + hash, + status, + symbol: asset.symbol, + }); } } else if (approvePermit2) { const allowance = await erc20.read.allowance([walletAddress, permit2.address], opts); @@ -228,7 +245,24 @@ export const GetEXAProvider: FC = ({ children }) => { gasLimit: gasLimit(gas), }); setTX({ status: 'processing', hash }); + track('TX Signed', { + contractName: 'ERC20', + method: 'approve', + amount: 'MAX_UINT256', + hash, + to: permit2.address, + symbol: asset.symbol, + }); const { status, transactionHash } = await waitForTransaction({ hash }); + track('TX Completed', { + contractName: 'ERC20', + method: 'approve', + amount: 'MAX_UINT256', + hash, + status, + to: permit2.address, + symbol: asset.symbol, + }); setTX({ status: status ? 'success' : 'error', hash: transactionHash }); } } @@ -371,7 +405,25 @@ export const GetEXAProvider: FC = ({ children }) => { ...crossChainOpts, gasLimit: gasLimit(gas), }); - await waitForTransaction({ hash }); + track('TX Signed', { + contractName: 'ERC20', + method: 'approve', + amount: formatUnits(minimumApprovalAmount, asset?.decimals || 18), + hash, + to: approvalData.allowanceTarget, + symbol: asset?.symbol, + }); + const { status } = await waitForTransaction({ hash }); + + track('TX Completed', { + contractName: 'ERC20', + method: 'approve', + amount: formatUnits(minimumApprovalAmount, asset?.decimals || 18), + hash, + to: approvalData.allowanceTarget, + status, + symbol: asset?.symbol, + }); } setTXStep(TXStep.CONFIRM); } catch (err) { @@ -380,7 +432,7 @@ export const GetEXAProvider: FC = ({ children }) => { } setTXStep(TXStep.APPROVE); } - }, [asset?.symbol, chain?.chainId, erc20, opts, route, screen, walletAddress, walletClient]); + }, [asset, chain?.chainId, erc20, opts, route, screen, walletAddress, walletClient]); const nativeSwap = asset?.symbol === 'ETH' && chain?.chainId === optimism.id; const qtyOut = @@ -407,14 +459,34 @@ export const GetEXAProvider: FC = ({ children }) => { }); setTX({ status: 'processing', hash: txHash_ }); + + track('TX Signed', { + contractName: 'SocketGateway', + method: 'swap', + amount: qtyIn, + symbol: asset?.symbol, + hash: txHash_, + to: txTarget, + }); + const { status, transactionHash } = await waitForTransaction({ hash: txHash_ }); + + track('TX Completed', { + contractName: 'ERC20', + method: 'approve', + amount: qtyIn, + symbol: asset?.symbol, + hash: transactionHash, + to: txTarget, + status, + }); setTX({ status: status ? 'success' : 'error', hash: transactionHash }); setScreen(Screen.TX_STATUS); } catch (err) { setTXError({ status: true, message: handleOperationError(err) }); setTXStep(TXStep.CONFIRM); } - }, [destinationCallData, route, txStep, walletClient]); + }, [asset?.symbol, destinationCallData, qtyIn, route, txStep, walletClient]); const socketSubmit = useCallback(async () => { const minEXA = 0n; diff --git a/utils/segment.ts b/utils/segment.ts index 0d119a474..096a2fe53 100644 --- a/utils/segment.ts +++ b/utils/segment.ts @@ -12,7 +12,7 @@ type TrackEvent = { 'Button Clicked': { name: string; location: string; - icon?: 'Close' | 'Edit' | 'Settings' | 'Copy' | 'Menu' | 'Replay'; + icon?: 'Close' | 'Edit' | 'Settings' | 'Copy' | 'Menu' | 'Replay' | 'ArrowBack'; href?: string; }; 'Option Selected': {