GovRepresentativesModalContent.tsx
1 import { ChainId } from '@aave/contract-helpers'; 2 import { t, Trans } from '@lingui/macro'; 3 import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; 4 import { Box, Checkbox, FormControlLabel, OutlinedInput, Stack, Typography } from '@mui/material'; 5 import { isAddress, parseUnits } from 'ethers/lib/utils'; 6 import { useState } from 'react'; 7 import { useModalContext } from 'src/hooks/useModal'; 8 import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; 9 import { ZERO_ADDRESS } from 'src/modules/governance/utils/formatProposal'; 10 import { useRootStore } from 'src/store/root'; 11 import { governanceV3Config } from 'src/ui-config/governanceConfig'; 12 import { getNetworkConfig, networkConfigs } from 'src/utils/marketsAndNetworksConfig'; 13 import { useShallow } from 'zustand/shallow'; 14 15 import { BaseSuccessView } from '../FlowCommons/BaseSuccess'; 16 import { GasEstimationError } from '../FlowCommons/GasEstimationError'; 17 import { TxModalTitle } from '../FlowCommons/TxModalTitle'; 18 import { GasStation } from '../GasStation/GasStation'; 19 import { ChangeNetworkWarning } from '../Warnings/ChangeNetworkWarning'; 20 import { GovRepresentativesActions } from './GovRepresentativesActions'; 21 22 export interface UIRepresentative { 23 chainId: ChainId; 24 representative: string; 25 remove: boolean; 26 invalid?: boolean; 27 } 28 29 export const GovRepresentativesContent = ({ 30 representatives, 31 }: { 32 representatives: Array<{ chainId: ChainId; representative: string }>; 33 }) => { 34 const { mainTxState, txError } = useModalContext(); 35 const { chainId: connectedChainId, readOnlyModeAddress } = useWeb3Context(); 36 const [currentNetworkConfig, currentChainId] = useRootStore( 37 useShallow((state) => [state.currentNetworkConfig, state.currentChainId]) 38 ); 39 const [reps, setReps] = useState<UIRepresentative[]>( 40 representatives.map((r) => { 41 if (r.representative === ZERO_ADDRESS) { 42 return { ...r, representative: '', remove: false }; 43 } else { 44 return { ...r, remove: false }; 45 } 46 }) 47 ); 48 49 // is Network mismatched 50 const govChain = 51 currentNetworkConfig.isFork && 52 currentNetworkConfig.underlyingChainId === governanceV3Config.coreChainId 53 ? currentChainId 54 : governanceV3Config.coreChainId; 55 const isWrongNetwork = connectedChainId !== govChain; 56 57 const networkConfig = getNetworkConfig(govChain); 58 59 const handleChange = (value: string, i: number) => { 60 const valid = isAddress(value); 61 62 setReps((prev) => { 63 const newReps = [...prev]; 64 newReps[i].representative = value; 65 newReps[i].invalid = value !== '' && !valid; 66 return newReps; 67 }); 68 }; 69 70 const blocked = reps.some( 71 (r) => r.representative !== '' && !r.remove && !isAddress(r.representative) 72 ); 73 74 const isDirty = reps.some((r) => { 75 const rep = representatives.find((re) => re.chainId === r.chainId); 76 // dirty if remvoing or changing address from initial value 77 if (!rep) return false; 78 79 return ( 80 (r.remove && rep.representative !== ZERO_ADDRESS) || 81 (rep.representative !== r.representative && r.representative !== '') 82 ); 83 }); 84 85 if (mainTxState.success) { 86 return ( 87 <BaseSuccessView txHash={mainTxState.txHash}> 88 <></> 89 </BaseSuccessView> 90 ); 91 } 92 93 return ( 94 <Box sx={{ m: -3 }}> 95 <Box sx={{ p: 3 }}> 96 <TxModalTitle title="Edit address" /> 97 </Box> 98 {isWrongNetwork && !readOnlyModeAddress && ( 99 <ChangeNetworkWarning networkName={networkConfig.name} chainId={govChain} /> 100 )} 101 <Stack direction="column" gap={2}> 102 {reps.map((r, i) => ( 103 <Box 104 key={i} 105 sx={(theme) => ({ 106 border: reps[i].remove 107 ? `1px solid ${theme.palette.action.active}` 108 : '1px solid transparent', 109 borderRadius: '8px', 110 background: reps[i].remove ? theme.palette.background.surface : 'transparent', 111 })} 112 > 113 <Stack gap={2} sx={{ px: 3, py: 3 }}> 114 <Stack direction="row" alignItems="center" justifyContent="space-between"> 115 <Stack direction="row" alignItems="center" gap={2}> 116 <img 117 src={networkConfigs[r.chainId].networkLogoPath} 118 height="16px" 119 width="16px" 120 alt="network logo" 121 /> 122 <Typography variant="description" color="text.secondary"> 123 {networkConfigs[r.chainId].name} 124 </Typography> 125 </Stack> 126 <FormControlLabel 127 sx={{ mr: 0 }} 128 label={ 129 <Typography sx={{ mr: 1 }} variant="subheader1" color="error.main"> 130 <Trans>Remove</Trans> 131 </Typography> 132 } 133 labelPlacement="start" 134 control={ 135 <Checkbox 136 sx={{ width: '16px', height: '16px' }} 137 checked={reps[i].remove} 138 onChange={(e) => { 139 setReps((prev) => { 140 const newReps = [...prev]; 141 newReps[i].remove = e.target.checked; 142 return newReps; 143 }); 144 }} 145 size="small" 146 /> 147 } 148 /> 149 </Stack> 150 <OutlinedInput 151 sx={{ height: '44px' }} 152 placeholder={t`Enter ETH address`} 153 value={r.representative} 154 error={r.invalid && !r.remove} 155 disabled={r.remove} 156 fullWidth 157 inputProps={{ sx: { py: 2, px: 3, fontSize: '14px' } }} 158 endAdornment={ 159 r.representative === '' || r.invalid ? null : ( 160 <CheckRoundedIcon fontSize="small" color="success" /> 161 ) 162 } 163 onChange={(e) => { 164 handleChange(e.target.value, i); 165 }} 166 /> 167 <Typography 168 sx={{ visibility: r.invalid && !r.remove ? 'visible' : 'hidden' }} 169 variant="helperText" 170 color="error.main" 171 > 172 <Trans>Can't validate the wallet address. Try again.</Trans> 173 </Typography> 174 </Stack> 175 </Box> 176 ))} 177 </Stack> 178 <Box sx={{ px: 3, pb: 3 }}> 179 <GasStation 180 disabled={blocked || !isDirty} 181 gasLimit={parseUnits('1000000', 'wei')} 182 chainId={governanceV3Config.coreChainId} 183 /> 184 {txError && <GasEstimationError txError={txError} />} 185 <GovRepresentativesActions 186 blocked={blocked || !isDirty} 187 isWrongNetwork={false} 188 representatives={reps} 189 /> 190 </Box> 191 </Box> 192 ); 193 };