/ src / components / transactions / GovRepresentatives / GovRepresentativesModalContent.tsx
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&apos;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  };