@chakra-ui/react#useToast TypeScript Examples

The following examples show how to use @chakra-ui/react#useToast. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: useDeleteFood.ts    From calories-in with MIT License 7 votes vote down vote up
function useDeleteFood({ food, onClose, onFoodDeleted }: Params) {
  const deleteConfirmationDisclosure = useDisclosure()
  const foodsActions = useFoodsActions()
  const toast = useToast()

  function onDelete() {
    deleteConfirmationDisclosure.onOpen()
  }

  function onConfirmDelete() {
    if (food) {
      foodsActions.removeFood(food.id)
      toast({
        position: 'top',
        title: 'Food deleted',
        status: 'success',
        duration: 2000,
        isClosable: true,
      })
      deleteConfirmationDisclosure.onClose()
      onFoodDeleted && onFoodDeleted(food)
      onClose()
    }
  }

  return {
    deleteConfirmationDisclosure,
    onDelete,
    onConfirmDelete,
  }
}
Example #2
Source File: create.tsx    From next-crud with MIT License 7 votes vote down vote up
UserCreate: NextPage = () => {
  const toast = useToast()
  const { replace } = useRouter()

  const onSubmit = async (values: IFormValues) => {
    try {
      await fetch(`/api/users`, {
        method: 'POST',
        body: JSON.stringify(values),
        headers: {
          'Content-Type': 'application/json',
        },
      })
      toast({
        status: 'success',
        description: 'User successfully created',
        duration: 2000,
      })
      replace('/users')
    } catch (e) {
      toast({
        status: 'error',
        description: 'Failed to create user',
        duration: 2000,
      })
    }
  }

  return (
    <Layout title="User create" backRoute="/users">
      <VStack spacing={4} width="100%">
        <Heading>User create</Heading>
        <UserForm onSubmit={onSubmit} />
      </VStack>
    </Layout>
  )
}
Example #3
Source File: FusePoolCreatePage.tsx    From rari-dApp with GNU Affero General Public License v3.0 6 votes vote down vote up
WhitelistInfo = ({
  whitelist,
  addToWhitelist,
  removeFromWhitelist,
}: {
  whitelist: string[];
  addToWhitelist: (user: string) => any;
  removeFromWhitelist: (user: string) => any;
}) => {
  const [_whitelistInput, _setWhitelistInput] = useState("");
  const { t } = useTranslation();
  const { fuse } = useRari();
  const toast = useToast();

  return (
    <>
      <OptionRow my={0} mb={4}>
        <Input
          width="100%"
          value={_whitelistInput}
          onChange={(event) => _setWhitelistInput(event.target.value)}
          placeholder="0x0000000000000000000000000000000000000000"
          _placeholder={{ color: "#FFF" }}
        />
        <IconButton
          flexShrink={0}
          aria-label="add"
          icon={<AddIcon />}
          width="35px"
          ml={2}
          bg="#282727"
          color="#FFF"
          borderWidth="1px"
          backgroundColor="transparent"
          onClick={() => {
            if (
              fuse.web3.utils.isAddress(_whitelistInput) &&
              !whitelist.includes(_whitelistInput)
            ) {
              addToWhitelist(_whitelistInput);
              _setWhitelistInput("");
            } else {
              toast({
                title: "Error!",
                description:
                  "This is not a valid ethereum address (or you have already entered this address)",
                status: "error",
                duration: 2000,
                isClosable: true,
                position: "top-right",
              });
            }
          }}
          _hover={{}}
          _active={{}}
        />
      </OptionRow>
      {whitelist.length > 0 ? (
        <Text mb={4} ml={4} width="100%">
          <b>{t("Already added:")} </b>
          {whitelist.map((user, index, array) => (
            <Text
              key={user}
              className="underline-on-hover"
              as="button"
              onClick={() => removeFromWhitelist(user)}
            >
              {user}
              {array.length - 1 === index ? null : <>,&nbsp;</>}
            </Text>
          ))}
        </Text>
      ) : null}
    </>
  );
}
Example #4
Source File: [id].tsx    From next-crud with MIT License 6 votes vote down vote up
UserCreate: NextPage<IProps> = ({ user }) => {
  const toast = useToast()
  const { replace } = useRouter()
  const queryClient = useQueryClient()

  const onSubmit = async (values: IFormValues) => {
    try {
      const userData = await fetch(`/api/users/${user.id}`, {
        method: 'PUT',
        body: JSON.stringify(values),
        headers: {
          'Content-Type': 'application/json',
        },
      }).then((res) => res.json())
      toast({
        status: 'success',
        description: 'User successfully updated',
        duration: 2000,
      })
      replace('/users')
      queryClient.setQueryData<
        InfiniteData<TPaginationResult<User>> | undefined
      >('users', (data) => {
        const page = data?.pages.find((page) =>
          page.data.some((userElem) => userElem.id === user.id)
        )
        if (page) {
          const elemIdx = page.data.findIndex((data) => data.id === user.id)
          page.data[elemIdx] = userData
        }

        return data
      })
    } catch (e) {
      toast({
        status: 'error',
        description: 'Failed to update user',
        duration: 2000,
      })
    }
  }

  return (
    <Layout title={user.username} backRoute="/users">
      <VStack spacing={4} width="100%">
        <Heading>User edition</Heading>
        <UserForm
          initialValues={{ username: user.username }}
          onSubmit={onSubmit}
        />
      </VStack>
    </Layout>
  )
}
Example #5
Source File: IconListItem.tsx    From lucide with ISC License 6 votes vote down vote up
IconListItem = ({ name, content, onClick, src: svg }: IconListItemProps) => {
  const toast = useToast();
  const { color, size, strokeWidth, iconsRef } = useCustomizeIconContext();

  return (
    <Button
      variant="ghost"
      borderWidth="1px"
      rounded="lg"
      padding={2}
      height={32}
      position="relative"
      whiteSpace="normal"
      onClick={event => {
        const src = iconsRef.current[name].outerHTML ?? svg
        if (event.shiftKey) {
          copy(src);
          toast({
            title: 'Copied!',
            description: `Icon "${name}" copied to clipboard.`,
            status: 'success',
            duration: 1500,
          });
        }
        if (event.altKey) {
          download(src, `${name}.\svg`, 'image/svg+xml');
        }
        if (onClick) {
          onClick(event);
        }
      }}
      key={name}
      alignItems="center"
    >
      <Flex direction="column" align="center" justify="stretch" width="100%" gap={4}>
        <Flex flex={2} flexBasis="100%" minHeight={10} align="flex-end">
          <IconWrapper
            content={content}
            stroke={color}
            strokeWidth={strokeWidth}
            height={size}
            width={size}
            ref={iconEl => (iconsRef.current[name] = iconEl)}
          />
        </Flex>
        <Flex flex={1} minHeight={10} align="center">
          <Text wordBreak="break-word" maxWidth="100%">
            {name}
          </Text>
        </Flex>
      </Flex>
    </Button>
  );
}
Example #6
Source File: home-page.tsx    From notebook with MIT License 6 votes vote down vote up
HomePage: React.SFC<HomePageProps> = ({ notes, setNotes }) => {
  const { isOpen, onOpen, onClose } = useDisclosure();
  const [selectedNote, setSelectedNote] = React.useState<note>();
  const toast = useToast();

  const handleClick = (id: string) => {
    const selectedNote = notes.find((note: note) => note.id === id);
    setSelectedNote(selectedNote);
    onOpen();
  };

  const handleNoteUpdate = (newNote: note) => {
    const newNotesState: note[] = [...notes];
    const index = notes.findIndex((note: note) => note.id === newNote.id);
    newNotesState[index] = newNote;
    setNotes(newNotesState);
    showToast();
  };

  const showToast = () => {
    toast({
      title: "Note updated.",
      status: "success",
      position: "top",
      duration: 2000,
      isClosable: true
    });
  };

  return (
    <>
      <AnimatePage>
        {notes.length ? (
          <NotesList
            notes={notes}
            handleClick={handleClick}
            setNotes={setNotes}
          />
        ) : (
          <HeroSection />
        )}
        {isOpen ? (
          <NoteForm
            isOpen={isOpen}
            onClose={onClose}
            handleNoteUpdate={handleNoteUpdate}
            selectedNote={selectedNote}
          />
        ) : (
          ""
        )}
      </AnimatePage>
    </>
  );
}
Example #7
Source File: index.tsx    From calories-in with MIT License 5 votes vote down vote up
function FoodsListModal({ onClose, isOpen, foodsToImport }: Props) {
  const title = foodsToImport ? 'Import Foods' : 'Export Foods'
  const { userFoods } = useFoods()
  const foodsActions = useFoodsActions()
  const toast = useToast()
  const foods = foodsToImport || userFoods

  function onImport() {
    if (foodsToImport) {
      foodsActions.setFoods(foodsToImport)
      toast({
        position: 'top',
        status: 'success',
        title: 'Foods imported',
        isClosable: true,
      })
      onClose()
    }
  }

  return (
    <Modal
      isOpen={isOpen}
      preserveScrollBarGap={true}
      onClose={onClose}
      scrollBehavior="inside"
    >
      <ModalOverlay />

      <FoodsFilterStoreProvider
        initialFilter={{ ...DEFAULT_FILTER, onlyFoodsAddedByUser: true }}
        shouldSaveFilter={false}
      >
        <FoodsStoreProvider initialFoods={foods}>
          <Content
            onClose={onClose}
            title={title}
            onImport={onImport}
            action={foodsToImport ? 'import' : 'export'}
          />
        </FoodsStoreProvider>
      </FoodsFilterStoreProvider>
    </Modal>
  )
}
Example #8
Source File: FusePoolPage.tsx    From rari-dApp with GNU Affero General Public License v3.0 5 votes vote down vote up
PendingAdminAlert = ({ comptroller }: { comptroller?: string }) => {
  const { address, fuse } = useRari();

  const toast = useToast();
  const queryClient = useQueryClient();

  const [isAccepting, setIsAccepting] = useState(false);

  const isPendingAdmin = useIsComptrollerPendingAdmin(comptroller);

  const acceptAdmin = async () => {
    if (!comptroller) return;
    const unitroller = createUnitroller(comptroller, fuse);
    setIsAccepting(true);

    try {
      await testForComptrollerErrorAndSend(
        unitroller.methods._acceptAdmin(),
        address,
        ""
      );

      LogRocket.track("Fuse-AcceptAdmin");

      queryClient.refetchQueries();
      setIsAccepting(false);
    } catch (e) {
      setIsAccepting(false);

      handleGenericError(e, toast);
    }
  };

  return (
    <>
      {isPendingAdmin && (
        <AdminAlert
          isAdmin={isPendingAdmin}
          isAdminText="You are the pending admin of this Fuse Pool! Click to Accept Admin"
          rightAdornment={
            <Button
              h="100%"
              p={3}
              ml="auto"
              color="black"
              onClick={acceptAdmin}
              disabled={isAccepting}
            >
              <HStack>
                <Text fontWeight="bold">
                  {isAccepting} ? Accepting... : Accept Admin{" "}
                </Text>
              </HStack>
            </Button>
          }
        />
      )}
    </>
  );
}
Example #9
Source File: AmountSelect.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
TokenNameAndMaxButton = ({
  updateAmount,
  logoURL,
  asset,
  mode,
  symbol,
  comptrollerAddress,
}: {
  logoURL: string;
  symbol: string;
  asset: USDPricedFuseAsset;
  mode: Mode;
  comptrollerAddress: string;
  updateAmount: (newAmount: string) => any;
}) => {
  const { fuse, address } = useRari();

  const toast = useToast();

  const [isMaxLoading, setIsMaxLoading] = useState(false);

  const setToMax = async () => {
    setIsMaxLoading(true);

    try {
      const maxBN = await fetchMaxAmount(mode, fuse, address, asset);

      if (maxBN!.isNeg() || maxBN!.isZero()) {
        updateAmount("");
      } else {
        const str = new BigNumber(maxBN!.toString())
          .div(10 ** asset.underlyingDecimals)
          .toFixed(18)
          // Remove trailing zeroes
          .replace(/\.?0+$/, "");

        updateAmount(str);
      }

      setIsMaxLoading(false);
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  const { t } = useTranslation();

  return (
    <Row
      mainAxisAlignment="flex-start"
      crossAxisAlignment="center"
      flexShrink={0}
    >
      <Row mainAxisAlignment="flex-start" crossAxisAlignment="center">
        <Box height="25px" width="25px" mb="2px" mr={2}>
          <Image
            width="100%"
            height="100%"
            borderRadius="50%"
            backgroundImage={`url(${SmallWhiteCircle})`}
            src={logoURL}
            alt=""
          />
        </Box>
        <Heading fontSize="24px" mr={2} flexShrink={0}>
          {symbol}
        </Heading>
      </Row>

      <Button
        ml={1}
        height="28px"
        width="58px"
        bg="transparent"
        border="2px"
        borderRadius="8px"
        borderColor="#272727"
        fontSize="sm"
        fontWeight="extrabold"
        _hover={{}}
        _active={{}}
        onClick={setToMax}
        isLoading={isMaxLoading}
      >
        {t("MAX")}
      </Button>
    </Row>
  );
}
Example #10
Source File: index.tsx    From coindrop with GNU General Public License v3.0 4 votes vote down vote up
UserDataForm: FC<UserDataFormProps> = ({ userData, mutate, userId }) => {
    const [isSubmitting, setIsSubmitting] = useState(false);
    const toast = useToast();
    const { register, handleSubmit, formState: { isDirty }, reset } = useForm();
    const email_lists = userData?.email_lists;
    const onSubmit = async (rawFormData) => {
        setIsSubmitting(true);
        const userDataForDb = {
            email_lists: [],
        };
        Object.keys(optionalEmailLists).forEach(emailListId => {
            if (rawFormData.email_lists[emailListId]) {
                userDataForDb.email_lists.push(emailListId);
            }
        });
        try {
            await updateUserData({ data: userDataForDb, userId });
            mutate(userDataForDb);
            reset();
            toast({
                title: "Account updated",
                status: "success",
                duration: 6000,
                isClosable: true,
            });
        } catch (err) {
            toast({
                title: "Error updating account",
                description: "Please try again or contact support",
                status: "error",
                duration: 9000,
                isClosable: true,
            });
        } finally {
            setIsSubmitting(false);
        }
    };
    return (
        <SectionContainer>
            <form onSubmit={handleSubmit(onSubmit)} data-testid="settings-form">
                <SectionHeading size="md">
                    E-mail
                </SectionHeading>
                <Box
                    id="email-preferences-content"
                    m={4}
                >
                    <FormLabel>Newsletters</FormLabel>
                    <Flex wrap="wrap">
                        {Object.entries(optionalEmailLists).map(([emailListId, emailListDisplayName]: [EmailListIds, string]) => {
                            return (
                                <Checkbox
                                    key={emailListId}
                                    mr={6}
                                    name={`email_lists.${emailListId}`}
                                    colorScheme="orange"
                                    defaultChecked={email_lists?.includes(emailListId)}
                                    ref={register()}
                                >
                                    {emailListDisplayName}
                                </Checkbox>
                            );
                        })}
                        {alwaysEnabledEmailLists.map(listName => (
                            <Checkbox
                                key={listName}
                                mr={6}
                                colorScheme="orange"
                                defaultChecked
                                isDisabled
                            >
                                {listName}
                            </Checkbox>
                        ))}
                    </Flex>
                </Box>
                <Box>
                    <Button
                        colorScheme="green"
                        type="submit"
                        isDisabled={!isDirty || isSubmitting}
                        leftIcon={isSubmitting ? <Spinner size="sm" /> : undefined}
                    >
                        {isSubmitting ? 'Saving' : 'Save'}
                    </Button>
                </Box>
            </form>
        </SectionContainer>
    );
}
Example #11
Source File: IconDetailOverlay.tsx    From lucide with ISC License 4 votes vote down vote up
IconDetailOverlay = ({ open = true, close, icon }) => {
  const toast = useToast();
  const { colorMode } = useColorMode();
  const { tags = [], name } = icon;
  const {color, strokeWidth, size} = useContext(IconStyleContext);
  const iconRef = useRef<SVGSVGElement>(null);
  const [isMobile] = useMediaQuery("(max-width: 560px)")
  const { isOpen, onOpen, onClose } = useDisclosure()

  const handleClose = () => {
    onClose();
    close();
  };

  useEffect(() => {
    if(open) {
      onOpen()
    }
  }, [open])

  const iconStyling = (isLight) => ({
    height: "25vw",
    width: "25vw",
    minHeight: "160px",
    minWidth: "160px",
    maxHeight: "240px",
    maxWidth: "240px",
    color: color,
  });

  const downloadIcon = ({src, name} : IconDownload) => download(src, `${name}.svg`, 'image/svg+xml');

  const copyIcon = ({src, name} : IconDownload) => {
    copy(src);
    toast({
      title: "Copied!",
      description: `Icon "${name}" copied to clipboard.`,
      status: "success",
      duration: 1500,
      isClosable: true
    });
  }

  const downloadPNG = ({src, name}: IconDownload) => {
    const canvas = document.createElement('canvas');
    canvas.width = size;
    canvas.height = size;
    const ctx = canvas.getContext("2d");

    const image = new Image();
    image.src = `data:image/svg+xml;base64,${btoa(src)}`;
    image.onload = function() {
      ctx.drawImage(image, 0, 0);

      const link = document.createElement('a');
      link.download = `${name}.png`;
      link.href = canvas.toDataURL('image/png')
      link.click();
    }
  }

  return (
    <Box
      position="fixed"
      bottom={0}
      zIndex={2}
      width="100%"
      left={0}
      height={0}
      key={name}
    >
      <Slide direction="bottom" in={isOpen} style={{ zIndex: 10 }}>
      <Flex
        alignItems="center"
        justifyContent="space-between"
        pt={4}
        pb={4}
        maxW="850px"
        margin="0 auto"
        w="full"
        px={8}
      >

          <Box
            borderWidth="1px"
            rounded="lg"
            width="full"
            boxShadow={theme.shadows.xl}
            position="relative"
            bg={
              colorMode == "light"
                ? theme.colors.white
                : theme.colors.gray[700]
            }
            padding={8}
          >
            <IconButton
              size="sm"
              aria-label="Close overlay"
              variant="ghost"
              color="current"
              ml="3"
              position="absolute"
              top={4}
              right={4}
              onClick={handleClose}
              icon={<Close />}
          />
            <Flex direction={['column', 'row']} alignItems={['center', 'flex-start']}>
              <Flex>
                <Box
                  borderWidth="1px"
                  rounded="md"
                  position="relative"
                  bg={
                    colorMode == "light"
                      ? theme.colors.whiteAlpha[800]
                      : theme.colors.blackAlpha[500]
                  }
                  padding={0}
                >
                  <div
                    style={iconStyling(colorMode == "light")}
                    className="icon-large"
                  >
                    <IconWrapper
                      content={icon.content}
                      stroke={color}
                      strokeWidth={strokeWidth}
                      height={size}
                      width={size}
                      ref={iconRef}
                    />
                  </div>

                  <svg className="icon-grid" width="24" height="24" viewBox={`0 0 ${size} ${size}`} fill="none" stroke={colorMode == "light" ? '#E2E8F0' : theme.colors.gray[600]} strokeWidth="0.1" xmlns="http://www.w3.org/2000/svg">
                    { Array.from({ length:(size - 1) }, (_, i) => (
                      <g key={`grid-${i}`}>
                        <line key={`horizontal-${i}`} x1={0} y1={i + 1} x2={size} y2={i + 1} />
                        <line key={`vertical-${i}`} x1={i + 1} y1={0} x2={i + 1} y2={size} />
                      </g>
                    )) }
                  </svg>
                </Box>
              </Flex>
              <Flex marginLeft={[0, 8]} w="100%">
                <Box w="100%">
                  <Flex
                    justify={isMobile ? 'center' : 'flex-start'}
                    marginTop={isMobile ? 10 : 0}
                  >
                    <Box
                      position="relative"
                      mb={1}
                      display="inline-block"
                      style={{ cursor: "pointer" }}
                      pr={6}
                    >
                      <Text fontSize="3xl">
                        {icon.name}
                      </Text>
                      { icon?.contributors?.length ? ( <ModifiedTooltip/> ) : null}
                    </Box>
                  </Flex>
                  <Box mb={4}>
                    { tags?.length ? (
                      <Text
                        fontSize="xl"
                        fontWeight="bold"
                        color={
                          colorMode === "light"
                          ? 'gray.600'
                          : 'gray.500'
                        }
                      >
                        { tags.join(' • ') }
                      </Text>
                    ) : ''}

                  {/* <Button size="sm" fontSize="md" variant="ghost" onClick={() => downloadIcon(icon)}>
                    Edit Tags
                  </Button> */}
                  </Box>
                  <Box overflowY="auto" w="100%" pt={1} pb={1}>
                    <ButtonGroup spacing={4}>
                      <Button variant="solid" onClick={() => downloadIcon({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
                        Download SVG
                      </Button>
                      <Button variant="solid" onClick={() => copyIcon({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
                        Copy SVG
                      </Button>
                      <Button variant="solid" onClick={() => downloadPNG({src: iconRef.current.outerHTML, name: icon.name})} mb={1}>
                        Download PNG
                      </Button>
                    </ButtonGroup>
                  </Box>
                  { icon?.contributors?.length ? (
                    <>
                      <Heading as="h5" size="sm" marginTop={4} marginBottom={2}>
                        Contributors:
                      </Heading>
                      <AvatarGroup size="md">
                        { icon.contributors.map((commit, index) => (
                          <Link href={`https://github.com/${commit.author}`} isExternal key={`${index}_${commit.sha}`}>
                            <Tooltip label={commit.author} key={commit.sha}>
                              <Avatar name={commit.author} src={`https://github.com/${commit.author}.png?size=88`} />
                            </Tooltip>
                          </Link>
                        )) }
                      </AvatarGroup>
                    </>
                  ) : null }
                </Box>
              </Flex>
            </Flex>
          </Box>

      </Flex>
      </Slide>
    </Box>
  );
}
Example #12
Source File: RenderDetail.tsx    From ke with MIT License 4 votes vote down vote up
RenderDetail = (props: RenderDetailProps): JSX.Element => {
  /*
    Entry point for displaying components in https://myspa.com/some-url/100500 route format.

    Here we fetch data from the backend using the url that we specified in a
    admin class.

    After that we mounts the widgets of a particular view type. At the moment there are two:
    - Detail View (see mountDetailFields for detail)
    - Wizard View (see mountWizards for detail)
  */
  const [mainDetailObject, setMainDetailObject] = useState<Model>()
  const [needRefreshDetailObject, setNeedRefreshDetailObject] = useState<boolean>(true)
  const { id } = useParams<{ id: string }>()
  const { resourceName, admin, provider, notifier } = props
  const toast = useToast()
  const detailNotifier = notifier || new ChakraUINotifier(toast)
  const [isLoading, setIsLoading] = useState<boolean>(true)
  const [loadError, setLoadError] = useState<LoadError | null>(null)
  const activeWizardRef = useRef<WizardControl>()

  let title = `${admin.verboseName} # ${id}`
  if (admin.getPageTitle) {
    const pageTitle = admin.getPageTitle(mainDetailObject)
    if (pageTitle) {
      title = pageTitle
    }
  }
  document.title = title

  let favicon = admin.favicon || ''

  if (admin.getPageFavicon) {
    const favIconSource = admin.getPageFavicon(mainDetailObject)
    if (favIconSource) {
      favicon = favIconSource
    }
  }
  setFavicon(favicon)

  const refreshMainDetailObject = (): void => {
    setNeedRefreshDetailObject(true)
  }

  useEffect(() => {
    const backendResourceUrl = admin.getResource(id)
    if (needRefreshDetailObject) {
      provider
        .getObject(backendResourceUrl)
        .then(async (res) => {
          setNeedRefreshDetailObject(false)
          setMainDetailObject(res)
          if (admin?.onDetailObjectLoaded !== undefined) {
            await admin.onDetailObjectLoaded({
              mainDetailObject: res,
              provider,
              context: containerStore,
              setInitialValue,
            })
          }
        })
        .catch((er: LoadError) => {
          setLoadError(er)
        })
        .finally(() => setIsLoading(false))
    }
  }, [id, provider, admin, needRefreshDetailObject, props, mainDetailObject])

  const { getDataTestId } = useCreateTestId({ name: admin.name })

  useEffect(() => {
    admin.onMount()
    return () => admin.onUnmount()
  }, [admin])

  return (
    <SaveEventProvider>
      <Row>
        <Col xs={12} xsOffset={0} md={10} mdOffset={1}>
          <Box padding="8px 0px">
            <ToListViewLink name={resourceName} />
          </Box>
        </Col>
      </Row>
      <Row {...getDataTestId()}>
        <Col xs={12} xsOffset={0} md={10} mdOffset={1}>
          {isLoading ? <Spinner /> : ''}
          {!isLoading && !loadError
            ? Object.entries(getContainersToMount()).map(([elementsKey, container]: [string, Function]) => {
                const elements = admin[elementsKey as keyof typeof admin]
                if (!elements) return []

                return (
                  <ErrorBoundary>
                    {container({
                      mainDetailObject,
                      setMainDetailObject,
                      ViewType,
                      elements,
                      elementsKey,
                      refreshMainDetailObject,
                      activeWizardRef,
                      ...props,
                      notifier: detailNotifier,
                    })}
                  </ErrorBoundary>
                )
              })
            : ''}
          {!isLoading && loadError ? (
            <Alert status="error" {...getDataTestId({ postfix: '--loadingError' })}>
              <AlertIcon />
              <AlertTitle mr={2}>Ошибка при выполнении запроса</AlertTitle>
              <AlertDescription>{loadError.response?.data?.message}</AlertDescription>
            </Alert>
          ) : (
            ''
          )}
        </Col>
      </Row>
    </SaveEventProvider>
  )
}
Example #13
Source File: RariContext.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
RariProvider = ({ children }: { children: ReactNode }) => {
  const { t } = useTranslation();

  const location = useLocation();

  const [rari, setRari] = useState<Rari>(
    () => new Rari(chooseBestWeb3Provider())
  );
  const [fuse, setFuse] = useState<Fuse>(() => initFuseWithProviders());

  const [isAttemptingLogin, setIsAttemptingLogin] = useState<boolean>(false);

  const toast = useToast();

  // Check the user's network:
  useEffect(() => {
    Promise.all([rari.web3.eth.net.getId(), rari.web3.eth.getChainId()]).then(
      ([netId, chainId]) => {
        console.log("Network ID: " + netId, "Chain ID: " + chainId);

        // Don't show "wrong network" toasts if dev
        if (process.env.NODE_ENV === "development") {
          return;
        }

        if (netId !== 1 || chainId !== 1) {
          // setTimeout(() => {
          //   toast({
          //     title: "Wrong network!",
          //     description:
          //       "You are on the wrong network! Switch to the mainnet and reload this page!",
          //     status: "warning",
          //     position: "top-right",
          //     duration: 300000,
          //     isClosable: true,
          //   });
          // }, 1500);
        }
      }
    );
  }, [rari, toast]);

  const [address, setAddress] = useState<string>(EmptyAddress);

  const [web3ModalProvider, setWeb3ModalProvider] = useState<any | null>(null);

  const queryClient = useQueryClient();

  const setRariAndAddressFromModal = useCallback(
    (modalProvider) => {
      const rariInstance = new Rari(modalProvider);
      const fuseInstance = initFuseWithProviders(modalProvider);
      window.Fuse = Fuse as any;

      setRari(rariInstance);
      setFuse(fuseInstance);

      rariInstance.web3.eth.getAccounts().then((addresses) => {
        if (addresses.length === 0) {
          console.log("Address array was empty. Reloading!");
          window.location.reload();
        }

        const address = addresses[0];
        const requestedAddress = new URLSearchParams(location.search).get(
          "address"
        );

        console.log("Setting Logrocket user to new address: " + address);
        LogRocket.identify(address);

        console.log("Requested address: ", requestedAddress);
        setAddress(requestedAddress ?? address);
      });
    },
    [setRari, setAddress, location.search]
  );

  const login = useCallback(
    async (cacheProvider: boolean = true) => {
      try {
        setIsAttemptingLogin(true);
        const provider = await launchModalLazy(t, cacheProvider);
        setWeb3ModalProvider(provider);
        setRariAndAddressFromModal(provider);
        setIsAttemptingLogin(false);
      } catch (err) {
        setIsAttemptingLogin(false);
        return console.error(err);
      }
    },
    [setWeb3ModalProvider, setRariAndAddressFromModal, setIsAttemptingLogin, t]
  );

  const refetchAccountData = useCallback(() => {
    console.log("New account, clearing the queryClient!");

    setRariAndAddressFromModal(web3ModalProvider);

    queryClient.clear();
  }, [setRariAndAddressFromModal, web3ModalProvider, queryClient]);

  const logout = useCallback(() => {
    setWeb3ModalProvider((past: any) => {
      if (past?.off) {
        past.off("accountsChanged", refetchAccountData);
        past.off("chainChanged", refetchAccountData);
      }

      return null;
    });

    localStorage.removeItem("WEB3_CONNECT_CACHED_PROVIDER");
    localStorage.removeItem("walletconnect");

    setAddress(EmptyAddress);
  }, [setWeb3ModalProvider, refetchAccountData]);

  useEffect(() => {
    if (web3ModalProvider !== null && web3ModalProvider.on) {
      web3ModalProvider.on("accountsChanged", refetchAccountData);
      web3ModalProvider.on("chainChanged", refetchAccountData);
    }

    return () => {
      if (web3ModalProvider?.off) {
        web3ModalProvider.off("accountsChanged", refetchAccountData);
        web3ModalProvider.off("chainChanged", refetchAccountData);
      }
    };
  }, [web3ModalProvider, refetchAccountData]);

  // Automatically open the web3modal if they have previously logged in on the site:
  useEffect(() => {
    if (localStorage.WEB3_CONNECT_CACHED_PROVIDER) {
      login();
    }
  }, [login]);

  const value = useMemo(
    () => ({
      web3ModalProvider,
      rari,
      fuse,
      isAuthed: address !== EmptyAddress,
      login,
      logout,
      address,
      isAttemptingLogin,
    }),
    [rari, web3ModalProvider, login, logout, address, fuse, isAttemptingLogin]
  );

  return <RariContext.Provider value={value}>{children}</RariContext.Provider>;
}
Example #14
Source File: ClaimRGTModal.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
ClaimRewards = ({ showPrivate }: { showPrivate: boolean }) => {
  const { fuse, address } = useRari();
  const { t } = useTranslation();

  const {
    rewardsDistributorsMap,
    unclaimed: unclaimedFuseRewards,
    rewardTokensMap,
  } = useUnclaimedFuseRewards();

  const { allClaimable, allRewardsTokens } = useClaimable(showPrivate);
  const toast = useToast();

  const [claimingAll, setClaimingAll] = useState(false);
  const [claimingToken, setClaimingToken] = useState<string | undefined>();

  const rewardTokensData = useTokensDataAsMap(allRewardsTokens);

  console.log({ allClaimable, rewardTokensData });

  const queryClient = useQueryClient();

  // Claims all Fuse LM rewards at once
  const handleClaimAll = useCallback(() => {
    setClaimingAll(true);

    // Claim from ALL available RDs
    claimRewardsFromRewardsDistributors(
      fuse,
      address,
      Object.keys(rewardsDistributorsMap)
    )
      .then(() => {
        queryClient.refetchQueries();
        setClaimingAll(false);
        toast({
          title: "Claimed All Rewards!",
          description: "",
          status: "success",
          duration: 2000,
          isClosable: true,
          position: "top-right",
        });
      })
      .catch((err) => {
        setClaimingAll(false);
        toast({
          title: "Error claiming rewards.",
          description: err.message,
          status: "error",
          duration: 2000,
          isClosable: true,
          position: "top-right",
        });
      });
  }, [fuse, address, rewardsDistributorsMap]);

  // Claims Fuse LM rewards for a single token
  const handleClaimFuseRewardsForToken = useCallback(
    (rewardToken: string) => {
      const rDs = rewardTokensMap[rewardToken];
      const rDAddresses = rDs.map((rD) => rD.rewardsDistributorAddress); // all rewardsdistributors for this token

      if (!!rDs.length) {
        setClaimingToken(rewardToken);
        claimRewardsFromRewardsDistributors(fuse, address, rDAddresses)
          .then(() => {
            setClaimingToken(undefined);
            toast({
              title: `Claimed All ${rewardTokensData[rewardToken].symbol} Rewards!`,
              description: "",
              status: "success",
              duration: 2000,
              isClosable: true,
              position: "top-right",
            });
          })
          .catch((err) => {
            setClaimingToken(undefined);
            toast({
              title: "Error claiming rewards.",
              description: err.message,
              status: "error",
              duration: 2000,
              isClosable: true,
              position: "top-right",
            });
          });
      }
    },
    [unclaimedFuseRewards, rewardsDistributorsMap, rewardTokensMap]
  );

  return (
    <Column
      mainAxisAlignment="flex-start"
      crossAxisAlignment="center"
      expand
      p={3}
    >
      {allClaimable.length ? (
        allClaimable.map((claimable) => {
          const pools =
            rewardTokensMap[claimable.unclaimed.rewardToken]?.reduce(
              (agg: number[], rD) => {
                return [...new Set([...agg, ...rD.pools])];
              },
              []
            ) ?? [];
          return (
            <ClaimableRow
              handleClaimFuseRewardsForToken={handleClaimFuseRewardsForToken}
              unclaimed={claimable.unclaimed}
              rewardTokenData={
                rewardTokensData[claimable.unclaimed.rewardToken]
              }
              mode={claimable.mode}
              claimingToken={claimingToken}
              pools={pools}
              my={1}
            />
          );
        })
      ) : (
        <Heading textAlign="center" size="md">
          No Claimable Rewards.
        </Heading>
      )}
      {!!allClaimable.length && (
        <GlowingButton
          onClick={() => handleClaimAll()}
          disabled={claimingAll}
          width="100%"
          height="51px"
          my={4}
        >
          {claimingAll ? <Spinner /> : t("Claim All")}
        </GlowingButton>
      )}
    </Column>
  );
}
Example #15
Source File: AmountSelect.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AmountSelect = ({ onClose, tranchePool, trancheRating }: Props) => {
  const token = tokens[tranchePool];

  const toast = useToast();

  const queryClient = useQueryClient();

  const { rari, address } = useRari();

  const { data: poolTokenBalance } = useTokenBalance(token.address);

  const { sfiBalance } = useSFIBalance();

  const { saffronPool } = useSaffronData();

  const { data: sfiRatio } = useQuery(tranchePool + " sfiRatio", async () => {
    return parseFloat(
      rari.web3.utils.fromWei(await saffronPool.methods.SFI_ratio().call())
    );
  });

  const [userAction, setUserAction] = useState(UserAction.NO_ACTION);

  const [userEnteredAmount, _setUserEnteredAmount] = useState("");

  const [amount, _setAmount] = useState<BigNumber | null>(
    () => new BigNumber(0)
  );

  const updateAmount = (newAmount: string) => {
    if (newAmount.startsWith("-")) {
      return;
    }

    _setUserEnteredAmount(newAmount);

    try {
      BigNumber.DEBUG = true;

      // Try to set the amount to BigNumber(newAmount):
      const bigAmount = new BigNumber(newAmount);
      _setAmount(bigAmount.multipliedBy(10 ** token.decimals));
    } catch (e) {
      // If the number was invalid, set the amount to null to disable confirming:
      _setAmount(null);
    }

    setUserAction(UserAction.NO_ACTION);
  };

  const amountIsValid = (() => {
    if (amount === null || amount.isZero()) {
      return false;
    }

    if (!poolTokenBalance) {
      return false;
    }

    return amount.lte(poolTokenBalance.toString());
  })();

  const sfiRequired = (() => {
    return amount && sfiRatio
      ? amount
          .div(10 ** token.decimals)
          .multipliedBy((1 / sfiRatio) * 10 ** SFIToken.decimals)
      : new BigNumber(0);
  })();

  const hasEnoughSFI = (() => {
    if (!requiresSFIStaking(trancheRating)) {
      return true;
    }

    if (!sfiBalance || sfiBalance.isZero()) {
      return false;
    }

    return sfiRequired.lte(sfiBalance.toString());
  })();

  const { t } = useTranslation();

  let depositOrWithdrawAlert;

  if (amount === null) {
    depositOrWithdrawAlert = t("Enter a valid amount to deposit.");
  } else if (amount.isZero()) {
    depositOrWithdrawAlert = t("Enter a valid amount to deposit.");
  } else if (!poolTokenBalance || !sfiBalance) {
    depositOrWithdrawAlert = t("Loading your balance of {{token}}...", {
      token: tranchePool,
    });
  } else if (!amountIsValid) {
    depositOrWithdrawAlert = t("You don't have enough {{token}}.", {
      token: tranchePool,
    });
  } else if (!hasEnoughSFI) {
    depositOrWithdrawAlert = t(
      "You need {{sfiMissing}} more SFI to deposit (1 SFI : {{sfiRatio}} {{tranchePool}})",
      {
        sfiRatio: sfiRatio ?? "?",
        tranchePool,
        sfiMissing: sfiRequired
          .minus(sfiBalance.toString())
          .div(10 ** SFIToken.decimals)
          .decimalPlaces(2)
          .toString(),
      }
    );
  } else {
    depositOrWithdrawAlert = t("Click confirm to continue!");
  }

  const onConfirm = async () => {
    try {
      //@ts-ignore
      const amountBN = rari.web3.utils.toBN(amount!.decimalPlaces(0));

      // Check A tranche cap
      if (trancheRating === TrancheRating.A) {
        const limits = await saffronPool.methods
          .get_available_S_balances()
          .call();

        const amountLeftBeforeCap = new BigNumber(limits[0] + limits[1]).div(
          10
        );

        if (amountLeftBeforeCap.lt(amountBN.toString())) {
          toast({
            title: "Error!",
            description: `The A tranche is capped at 1/10 the liquidity of the S tranche. Currently you must deposit less than ${amountLeftBeforeCap
              .div(10 ** token.decimals)
              .decimalPlaces(2)
              .toString()} ${
              token.symbol
            } or deposit into the S tranche (as more is deposited into S tranche, the cap on the A tranche increases).`,
            status: "error",
            duration: 18000,
            isClosable: true,
            position: "top-right",
          });

          return;
        }
      }

      // They must have already seen the quote as the button to trigger this function is disabled while it's loading:
      // This means they are now ready to start sending transactions:
      setUserAction(UserAction.WAITING_FOR_TRANSACTIONS);

      const poolAddress = saffronPool.options.address;

      const SFIContract = new rari.web3.eth.Contract(
        ERC20ABI as any,
        SFIToken.address
      );

      const trancheToken = new rari.web3.eth.Contract(
        ERC20ABI as any,
        token.address
      );

      const hasApprovedEnoughSFI = requiresSFIStaking(trancheRating)
        ? rari.web3.utils
            .toBN(
              await SFIContract.methods.allowance(address, poolAddress).call()
            )
            .gte(amountBN)
        : true;

      const hasApprovedEnoughPoolToken = rari.web3.utils
        .toBN(await trancheToken.methods.allowance(address, poolAddress).call())
        .gte(amountBN);

      if (!hasApprovedEnoughSFI) {
        // Approve the amount of poolToken because it will always be more than sfiRequired
        const txn = SFIContract.methods
          .approve(poolAddress, amountBN.toString())
          .send({ from: address });

        // If the user has already approved the poolToken we need to wait for this txn to complete before showing the add liquidity txn
        if (hasApprovedEnoughPoolToken) {
          await txn;
        }
      }

      if (!hasApprovedEnoughPoolToken) {
        // Approve tranche token (DAI or USDC)
        await trancheToken.methods
          .approve(saffronPool.options.address, amountBN.toString())
          .send({ from: address });
      }

      await saffronPool.methods
        .add_liquidity(amountBN.toString(), trancheRatingIndex(trancheRating))
        .send({ from: address });

      queryClient.refetchQueries();
      // Wait 2 seconds for refetch and then close modal.
      // We do this instead of waiting the refetch because some refetches take a while or error out and we want to close now.
      await new Promise((resolve) => setTimeout(resolve, 2000));
      onClose();
    } catch (e) {
      handleGenericError(e, toast);
      setUserAction(UserAction.NO_ACTION);
    }
  };

  return userAction === UserAction.WAITING_FOR_TRANSACTIONS ? (
    <Column expand mainAxisAlignment="center" crossAxisAlignment="center" p={4}>
      <HashLoader
        size={70}
        color={requiresSFIStaking(trancheRating) ? SFIToken.color : token.color}
        loading
      />
      <Heading mt="30px" textAlign="center" size="md">
        {t("Check your wallet to submit the transactions")}
      </Heading>
      <Text fontSize="sm" mt="15px" textAlign="center">
        {t("Do not close this tab until you submit all transactions!")}
      </Text>
    </Column>
  ) : (
    <>
      <Row
        width="100%"
        mainAxisAlignment="center"
        crossAxisAlignment="center"
        p={4}
      >
        <Heading fontSize="27px">
          {t("{{trancheRating}} Tranche Deposit", { trancheRating })}
        </Heading>
      </Row>
      <ModalDivider />
      <Column
        mainAxisAlignment="space-between"
        crossAxisAlignment="center"
        p={4}
        height="100%"
      >
        <Text fontWeight="bold" fontSize="sm" textAlign="center">
          {depositOrWithdrawAlert}
        </Text>
        <DashboardBox width="100%" height="70px">
          <Row
            p={4}
            mainAxisAlignment="space-between"
            crossAxisAlignment="center"
            expand
          >
            <AmountInput
              selectedToken={tranchePool}
              displayAmount={userEnteredAmount}
              updateAmount={updateAmount}
            />

            <TokenNameAndMaxButton
              selectedToken={tranchePool}
              updateAmount={updateAmount}
            />
          </Row>
        </DashboardBox>

        {requiresSFIStaking(trancheRating) ? (
          <DashboardBox width="100%" height="70px">
            <Row
              p={4}
              mainAxisAlignment="space-between"
              crossAxisAlignment="center"
              expand
            >
              <AmountInput
                selectedToken="SFI"
                displayAmount={
                  sfiRequired.isZero()
                    ? "0.0"
                    : sfiRequired.div(10 ** SFIToken.decimals).toString()
                }
                updateAmount={noop}
              />

              <TokenNameAndMaxButton selectedToken="SFI" updateAmount={noop} />
            </Row>
          </DashboardBox>
        ) : null}

        <Button
          fontWeight="bold"
          fontSize="2xl"
          borderRadius="10px"
          width="100%"
          height="70px"
          bg={requiresSFIStaking(trancheRating) ? SFIToken.color : token.color}
          _hover={{ transform: "scale(1.02)" }}
          _active={{ transform: "scale(0.95)" }}
          color={token.overlayTextColor}
          onClick={onConfirm}
          isLoading={!poolTokenBalance}
          isDisabled={!amountIsValid || !hasEnoughSFI}
        >
          {t("Confirm")}
        </Button>
      </Column>
    </>
  );
}
Example #16
Source File: AmountSelect.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AmountSelect = ({
  selectedToken,
  openCoinSelect,
  mode,
  openOptions,
  onClose,
}: Props) => {
  const token = tokens[selectedToken];

  const poolType = usePoolType();

  const { rari, address } = useRari();

  const {
    data: selectedTokenBalance,
    isLoading: isSelectedTokenBalanceLoading,
  } = useTokenBalance(token.address);

  const [userAction, setUserAction] = useState(UserAction.NO_ACTION);

  const [quoteAmount, setQuoteAmount] = useState<null | BN>(null);

  const [userEnteredAmount, _setUserEnteredAmount] = useState("");

  const [amount, _setAmount] = useState<BigNumber | null>(
    () => new BigNumber(0)
  );

  const updateAmount = (newAmount: string) => {
    if (newAmount.startsWith("-")) {
      return;
    }

    _setUserEnteredAmount(newAmount);

    try {
      BigNumber.DEBUG = true;

      // Try to set the amount to BigNumber(newAmount):
      const bigAmount = new BigNumber(newAmount);
      _setAmount(bigAmount.multipliedBy(10 ** token.decimals));
    } catch (e) {
      console.log(e);

      // If the number was invalid, set the amount to null to disable confirming:
      _setAmount(null);
    }

    setUserAction(UserAction.NO_ACTION);
  };

  const { max, isMaxLoading } = useMaxWithdraw(token.symbol);

  const amountIsValid = (() => {
    if (amount === null || amount.isZero()) {
      return false;
    }

    if (mode === Mode.DEPOSIT) {
      if (isSelectedTokenBalanceLoading) {
        return false;
      }

      return amount.lte(selectedTokenBalance!.toString());
    } else {
      if (isMaxLoading) {
        return false;
      }

      return amount.lte(max!.toString());
    }
  })();

  const { t } = useTranslation();

  let depositOrWithdrawAlert;

  if (amount === null) {
    depositOrWithdrawAlert =
      mode === Mode.DEPOSIT
        ? t("Enter a valid amount to deposit.")
        : t("Enter a valid amount to withdraw.");
  } else if (amount.isZero()) {
    if (poolType === Pool.ETH) {
      depositOrWithdrawAlert =
        mode === Mode.DEPOSIT
          ? t("Enter a valid amount to deposit.")
          : t("Enter a valid amount to withdraw.");
    } else {
      depositOrWithdrawAlert =
        mode === Mode.DEPOSIT
          ? t("Choose which token you want to deposit.")
          : t("Choose which token you want to withdraw.");
    }
  } else if (isSelectedTokenBalanceLoading) {
    depositOrWithdrawAlert = t("Loading your balance of {{token}}...", {
      token: selectedToken,
    });
  } else if (!amountIsValid) {
    depositOrWithdrawAlert =
      mode === Mode.DEPOSIT
        ? t("You don't have enough {{token}}.", {
            token: selectedToken,
          })
        : t("You cannot withdraw this much {{token}}.", {
            token: selectedToken,
          });
  } else {
    if (poolType === Pool.YIELD) {
      depositOrWithdrawAlert = t(
        "This pool has withdrawal & interest fees. Click to learn more."
      );
    } else {
      if (mode === Mode.DEPOSIT) {
        depositOrWithdrawAlert = t(
          "This pool has performance fees. Click to learn more."
        );
      } else {
        depositOrWithdrawAlert = t("Click review + confirm to withdraw!");
      }
    }
  }

  const toast = useToast();

  const queryClient = useQueryClient();

  const onConfirm = async () => {
    try {
      const pool = getSDKPool({ rari, pool: poolType });

      //@ts-ignore
      const amountBN = rari.web3.utils.toBN(amount!.decimalPlaces(0));

      // If clicking for the first time:
      if (userAction === UserAction.NO_ACTION) {
        setUserAction(UserAction.REQUESTED_QUOTE);

        let quote: BN;
        let slippage: BN;

        if (mode === Mode.DEPOSIT) {
          const [amountToBeAdded, , _slippage] =
            (await pool.deposits.validateDeposit(
              token.symbol,
              amountBN,
              address,
              true
            )) as BN[];

          quote = amountToBeAdded;
          slippage = _slippage;
        } else {
          const [amountToBeRemoved, , _slippage] =
            (await pool.withdrawals.validateWithdrawal(
              token.symbol,
              amountBN,
              address,
              true
            )) as BN[];

          quote = amountToBeRemoved;
          slippage = _slippage;
        }

        if (slippage) {
          const slippagePercent = (parseInt(slippage.toString()) / 1e18) * 100;
          const formattedSlippage = slippagePercent.toFixed(2) + "%";

          console.log("Slippage of " + formattedSlippage);

          // If slippage is >4% and user does not want to continue:
          if (
            slippagePercent > 4 &&
            !window.confirm(
              t(
                "High slippage of {{formattedSlippage}} for {{token}}, do you still wish to continue with this transaction?",
                { formattedSlippage, token: token.symbol }
              )
            )
          ) {
            setUserAction(UserAction.NO_ACTION);
            return;
          }
        }

        setQuoteAmount(quote);

        setUserAction(UserAction.VIEWING_QUOTE);

        return;
      }

      // They must have already seen the quote as the button to trigger this function is disabled while it's loading:
      // This means they are now ready to start sending transactions:

      setUserAction(UserAction.WAITING_FOR_TRANSACTIONS);

      if (mode === Mode.DEPOSIT) {
        // (Third item in array is approvalReceipt)
        const [, , , depositReceipt] = await pool.deposits.deposit(
          token.symbol,
          amountBN,
          quoteAmount!,
          {
            from: address,
          }
        );

        if (!depositReceipt) {
          throw new Error(
            t(
              "Prices and/or slippage have changed. Please reload the page and try again. If the problem persists, please contact us."
            )
          );
        }
      } else {
        // (Third item in array is withdrawReceipt)
        await pool.withdrawals.withdraw(token.symbol, amountBN, quoteAmount!, {
          from: address,
        });
      }

      queryClient.refetchQueries();
      // Wait 2 seconds for refetch and then close modal.
      // We do this instead of waiting the refetch because some refetches take a while or error out and we want to close now.
      await new Promise((resolve) => setTimeout(resolve, 2000));

      onClose();
    } catch (e) {
      handleGenericError(e, toast);
      setUserAction(UserAction.NO_ACTION);
    }
  };

  return userAction === UserAction.WAITING_FOR_TRANSACTIONS ? (
    <Column expand mainAxisAlignment="center" crossAxisAlignment="center" p={4}>
      <HashLoader size={70} color={token.color} loading />
      <Heading mt="30px" textAlign="center" size="md">
        {mode === Mode.DEPOSIT
          ? t("Check your wallet to submit the transactions")
          : t("Check your wallet to submit the transaction")}
      </Heading>
      <Text fontSize="sm" mt="15px" textAlign="center">
        {mode === Mode.DEPOSIT
          ? t("Do not close this tab until you submit both transactions!")
          : t("You may close this tab after submitting the transaction.")}
      </Text>
      <Text fontSize="xs" mt="5px" textAlign="center">
        {t(
          "Do not increase the price of gas more than 1.5x the prefilled amount!"
        )}
      </Text>
    </Column>
  ) : (
    <>
      <Row
        width="100%"
        mainAxisAlignment="space-between"
        crossAxisAlignment="center"
        p={4}
      >
        <Box width="40px" />
        <Heading fontSize="27px">
          {mode === Mode.DEPOSIT ? t("Deposit") : t("Withdraw")}
        </Heading>
        <IconButton
          color="#FFFFFF"
          variant="ghost"
          aria-label="Options"
          icon={<SettingsIcon />}
          _hover={{
            transform: "rotate(360deg)",
            transition: "all 0.7s ease-in-out",
          }}
          _active={{}}
          onClick={openOptions}
        />
      </Row>
      <ModalDivider />
      <Column
        mainAxisAlignment="space-between"
        crossAxisAlignment="center"
        p={4}
        height="100%"
      >
        <Text fontWeight="bold" fontSize="13px" textAlign="center">
          <Link
            href="https://www.notion.so/Fees-e4689d7b800f485098548dd9e9d0a69f"
            isExternal
          >
            {depositOrWithdrawAlert}
          </Link>
        </Text>
        <DashboardBox width="100%" height="70px">
          <Row
            p={4}
            mainAxisAlignment="space-between"
            crossAxisAlignment="center"
            expand
          >
            <AmountInput
              selectedToken={selectedToken}
              displayAmount={userEnteredAmount}
              updateAmount={updateAmount}
            />

            <TokenNameAndMaxButton
              openCoinSelect={openCoinSelect}
              selectedToken={selectedToken}
              updateAmount={updateAmount}
              mode={mode}
            />
          </Row>
        </DashboardBox>

        <Button
          fontWeight="bold"
          fontSize="2xl"
          borderRadius="10px"
          width="100%"
          height="70px"
          bg={token.color}
          _hover={{ transform: "scale(1.02)" }}
          _active={{ transform: "scale(0.95)" }}
          color={token.overlayTextColor}
          isLoading={
            isSelectedTokenBalanceLoading ||
            userAction === UserAction.REQUESTED_QUOTE
          }
          onClick={onConfirm}
          isDisabled={!amountIsValid}
        >
          {userAction === UserAction.VIEWING_QUOTE ? t("Confirm") : t("Review")}
        </Button>

        {poolHasDivergenceRisk(poolType) ? (
          <Link
            href="https://www.notion.so/Capital-Allocation-Risks-f4bccf324a594f46b849e6358e0a2464#631d223f598b42e28f9758541c1b1525"
            isExternal
          >
            <Text fontSize="xs" textAlign="center">
              {t(
                "You may experience divergence loss in this pool. Click for more info."
              )}
            </Text>
          </Link>
        ) : null}
      </Column>
      {userAction === UserAction.VIEWING_QUOTE ? (
        <ApprovalNotch color={token.color} mode={mode} amount={quoteAmount!} />
      ) : null}
    </>
  );
}
Example #17
Source File: AmountSelect.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AmountSelect = ({ onClose, mode, openOptions }: Props) => {
  const toast = useToast();

  const queryClient = useQueryClient();

  const [userAction, setUserAction] = useState(UserAction.NO_ACTION);

  const [userEnteredAmount, _setUserEnteredAmount] = useState("");

  const [amount, _setAmount] = useState<BigNumber | null>(
    () => new BigNumber(0)
  );

  const { t } = useTranslation();

  const { rari, address } = useRari();

  const { data: balance } = useTokenBalance(LP_TOKEN_CONTRACT);

  const { data: staked } = useQuery(address + "pool2BalanceBN", () => {
    return rari.governance.rgt.sushiSwapDistributions.stakingBalanceOf(address);
  });

  const updateAmount = (newAmount: string) => {
    if (newAmount.startsWith("-")) {
      return;
    }

    _setUserEnteredAmount(newAmount);

    try {
      BigNumber.DEBUG = true;

      // Try to set the amount to BigNumber(newAmount):
      const bigAmount = new BigNumber(newAmount);
      _setAmount(bigAmount.multipliedBy(10 ** 18));
    } catch (e) {
      // If the number was invalid, set the amount to null to disable confirming:
      _setAmount(null);
    }

    setUserAction(UserAction.NO_ACTION);
  };

  const amountIsValid = (() => {
    if (amount === null || amount.isZero()) {
      return false;
    }

    if (!balance || !staked) {
      return false;
    }

    if (mode === Mode.DEPOSIT) {
      return amount.lte(balance.toString());
    } else {
      return amount.lte(staked.toString());
    }
  })();

  let depositOrWithdrawAlert;

  if (amount === null || amount.isZero()) {
    if (mode === Mode.DEPOSIT) {
      depositOrWithdrawAlert = t("Enter a valid amount to deposit.");
    } else if (mode === Mode.WITHDRAW) {
      depositOrWithdrawAlert = t("Enter a valid amount to withdraw.");
    }
  } else if (!balance) {
    depositOrWithdrawAlert = t("Loading your balance of {{token}}...", {
      token: "ETH-RGT SLP",
    });
  } else if (!amountIsValid) {
    depositOrWithdrawAlert = t("You don't have enough {{token}}.", {
      token: "ETH-RGT SLP",
    });
  } else {
    depositOrWithdrawAlert = t("Click confirm to continue!");
  }

  const onConfirm = async () => {
    try {
      setUserAction(UserAction.WAITING_FOR_TRANSACTIONS);

      //@ts-ignore
      const amountBN = rari.web3.utils.toBN(amount!.decimalPlaces(0));

      if (mode === Mode.DEPOSIT) {
        await rari.governance.rgt.sushiSwapDistributions.deposit(amountBN, {
          from: address,
        });
      } else {
        await rari.governance.rgt.sushiSwapDistributions.withdraw(amountBN, {
          from: address,
        });
      }

      queryClient.refetchQueries();
      // Wait 2 seconds for refetch and then close modal.
      // We do this instead of waiting the refetch because some refetches take a while or error out and we want to close now.
      await new Promise((resolve) => setTimeout(resolve, 2000));
      onClose();
    } catch (e) {
      handleGenericError(e, toast);
      setUserAction(UserAction.NO_ACTION);
    }
  };

  return userAction === UserAction.WAITING_FOR_TRANSACTIONS ? (
    <Column expand mainAxisAlignment="center" crossAxisAlignment="center" p={4}>
      <HashLoader size={70} color={"#929192"} loading />
      <Heading mt="30px" textAlign="center" size="md">
        {t("Check your wallet to submit the transactions")}
      </Heading>
      <Text fontSize="sm" mt="15px" textAlign="center">
        {mode === Mode.DEPOSIT
          ? t("Do not close this tab until you submit both transactions!")
          : t("You may close this tab after submitting the transaction.")}
      </Text>
    </Column>
  ) : (
    <>
      <Row
        width="100%"
        mainAxisAlignment="space-between"
        crossAxisAlignment="center"
        p={4}
      >
        <Box width="40px" />
        <Heading fontSize="27px">
          {mode === Mode.DEPOSIT ? t("Deposit") : t("Withdraw")}
        </Heading>
        <IconButton
          color="#FFFFFF"
          variant="ghost"
          aria-label="Options"
          icon={<SettingsIcon />}
          _hover={{
            transform: "rotate(360deg)",
            transition: "all 0.7s ease-in-out",
          }}
          _active={{}}
          onClick={openOptions}
        />
      </Row>

      <ModalDivider />

      <Column
        mainAxisAlignment="space-between"
        crossAxisAlignment="center"
        p={4}
        height="100%"
      >
        <Text fontWeight="bold" fontSize="sm" textAlign="center">
          <Link
            href="https://www.notion.so/Fees-e4689d7b800f485098548dd9e9d0a69f"
            isExternal
          >
            {depositOrWithdrawAlert}
          </Link>
        </Text>

        <DashboardBox width="100%" height="70px" mt={4}>
          <Row
            p={4}
            mainAxisAlignment="space-between"
            crossAxisAlignment="center"
            expand
          >
            <AmountInput
              displayAmount={userEnteredAmount}
              updateAmount={updateAmount}
            />

            <TokenNameAndMaxButton updateAmount={updateAmount} mode={mode} />
          </Row>
        </DashboardBox>

        <Button
          mt={4}
          fontWeight="bold"
          fontSize="2xl"
          borderRadius="10px"
          width="100%"
          height="70px"
          bg={"#929192"}
          _hover={{ transform: "scale(1.02)" }}
          _active={{ transform: "scale(0.95)" }}
          color={"#FFF"}
          onClick={onConfirm}
          isLoading={!balance}
          isDisabled={!amountIsValid}
        >
          {t("Confirm")}
        </Button>
      </Column>
    </>
  );
}
Example #18
Source File: AmountSelect.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AmountSelect = ({
  onClose,
  assets,
  index,
  mode,
  setMode,

  comptrollerAddress,
  isBorrowPaused = false
}: {
  onClose: () => any;
  assets: USDPricedFuseAsset[];
  index: number;
  mode: Mode;
  setMode: (mode: Mode) => any;
  comptrollerAddress: string;
  isBorrowPaused?: boolean;
}) => {
  const asset = assets[index];

  const { address, fuse } = useRari();

  const toast = useToast();

  const queryClient = useQueryClient();

  const tokenData = useTokenData(asset.underlyingToken);

  const [userAction, setUserAction] = useState(UserAction.NO_ACTION);

  const [userEnteredAmount, _setUserEnteredAmount] = useState("");

  const [amount, _setAmount] = useState<BigNumber | null>(
    () => new BigNumber(0)
  );

  const showEnableAsCollateral = !asset.membership && mode === Mode.SUPPLY;
  const [enableAsCollateral, setEnableAsCollateral] = useState(
    showEnableAsCollateral
  );

  const { t } = useTranslation();

  const updateAmount = (newAmount: string) => {
    if (newAmount.startsWith("-")) {
      return;
    }

    _setUserEnteredAmount(newAmount);

    try {
      BigNumber.DEBUG = true;

      // Try to set the amount to BigNumber(newAmount):
      const bigAmount = new BigNumber(newAmount);
      _setAmount(bigAmount.multipliedBy(10 ** asset.underlyingDecimals));
    } catch (e) {
      // If the number was invalid, set the amount to null to disable confirming:
      _setAmount(null);
    }

    setUserAction(UserAction.NO_ACTION);
  };

  const { data: amountIsValid } = useQuery(
    (amount?.toString() ?? "null") + " " + mode + " isValid",
    async () => {
      if (amount === null || amount.isZero()) {
        return false;
      }

      try {
        const max = await fetchMaxAmount(mode, fuse, address, asset);

        return amount.lte(max!.toString());
      } catch (e) {
        handleGenericError(e, toast);
        return false;
      }
    }
  );

  let depositOrWithdrawAlert = null;

  if (mode === Mode.BORROW && isBorrowPaused) {
    depositOrWithdrawAlert = t("Borrowing is disabled for this asset.");
  }
  else if (amount === null || amount.isZero()) {
    if (mode === Mode.SUPPLY) {
      depositOrWithdrawAlert = t("Enter a valid amount to supply.");
    } else if (mode === Mode.BORROW) {
      depositOrWithdrawAlert = t("Enter a valid amount to borrow.");
    } else if (mode === Mode.WITHDRAW) {
      depositOrWithdrawAlert = t("Enter a valid amount to withdraw.");
    } else {
      depositOrWithdrawAlert = t("Enter a valid amount to repay.");
    }
  } else if (amountIsValid === undefined) {
    depositOrWithdrawAlert = t("Loading your balance of {{token}}...", {
      token: asset.underlyingSymbol,
    });
  } else if (!amountIsValid) {
    if (mode === Mode.SUPPLY) {
      depositOrWithdrawAlert = t("You don't have enough {{token}}!", {
        token: asset.underlyingSymbol,
      });
    } else if (mode === Mode.REPAY) {
      depositOrWithdrawAlert = t(
        "You don't have enough {{token}} or are over-repaying!",
        {
          token: asset.underlyingSymbol,
        }
      );
    } else if (mode === Mode.WITHDRAW) {
      depositOrWithdrawAlert = t("You cannot withdraw this much!");
    } else if (mode === Mode.BORROW) {
      depositOrWithdrawAlert = t("You cannot borrow this much!");
    }
  } else {
    depositOrWithdrawAlert = null;
  }

  const isMobile = useIsMobile();

  const length = depositOrWithdrawAlert?.length ?? 0;
  let depositOrWithdrawAlertFontSize;
  if (length < 40) {
    depositOrWithdrawAlertFontSize = !isMobile ? "xl" : "17px";
  } else if (length < 50) {
    depositOrWithdrawAlertFontSize = !isMobile ? "15px" : "11px";
  } else if (length < 60) {
    depositOrWithdrawAlertFontSize = !isMobile ? "14px" : "10px";
  }

  const onConfirm = async () => {
    try {
      setUserAction(UserAction.WAITING_FOR_TRANSACTIONS);

      const isETH = asset.underlyingToken === ETH_TOKEN_DATA.address;
      const isRepayingMax =
        amount!.eq(asset.borrowBalance) && !isETH && mode === Mode.REPAY;

      isRepayingMax && console.log("Using max repay!");

      const max = new BigNumber(2).pow(256).minus(1).toFixed(0);

      const amountBN = fuse.web3.utils.toBN(amount!.toFixed(0));

      const cToken = new fuse.web3.eth.Contract(
        isETH
          ? JSON.parse(
            fuse.compoundContracts[
              "contracts/CEtherDelegate.sol:CEtherDelegate"
            ].abi
          )
          : JSON.parse(
            fuse.compoundContracts[
              "contracts/CErc20Delegate.sol:CErc20Delegate"
            ].abi
          ),
        asset.cToken
      );

      if (mode === Mode.SUPPLY || mode === Mode.REPAY) {
        if (!isETH) {
          const token = new fuse.web3.eth.Contract(
            JSON.parse(
              fuse.compoundContracts[
                "contracts/EIP20Interface.sol:EIP20Interface"
              ].abi
            ),
            asset.underlyingToken
          );

          const hasApprovedEnough = fuse.web3.utils
            .toBN(
              await token.methods
                .allowance(address, cToken.options.address)
                .call()
            )
            .gte(amountBN);

          if (!hasApprovedEnough) {
            await token.methods
              .approve(cToken.options.address, max)
              .send({ from: address });
          }

          LogRocket.track("Fuse-Approve");
        }

        if (mode === Mode.SUPPLY) {
          // If they want to enable as collateral now, enter the market:
          if (enableAsCollateral) {
            const comptroller = createComptroller(comptrollerAddress, fuse);
            // Don't await this, we don't care if it gets executed first!
            comptroller.methods
              .enterMarkets([asset.cToken])
              .send({ from: address });

            LogRocket.track("Fuse-ToggleCollateral");
          }

          if (isETH) {
            const call = cToken.methods.mint();

            if (
              // If they are supplying their whole balance:
              amountBN.toString() === (await fuse.web3.eth.getBalance(address))
            ) {
              // Subtract gas for max ETH

              const { gasWEI, gasPrice, estimatedGas } = await fetchGasForCall(
                call,
                amountBN,
                fuse,
                address
              );

              await call.send({
                from: address,
                value: amountBN.sub(gasWEI),

                gasPrice,
                gas: estimatedGas,
              });
            } else {
              await call.send({
                from: address,
                value: amountBN,
              });
            }
          } else {
            await testForCTokenErrorAndSend(
              cToken.methods.mint(amountBN),
              address,
              "Cannot deposit this amount right now!"
            );
          }

          LogRocket.track("Fuse-Supply");
        } else if (mode === Mode.REPAY) {
          if (isETH) {
            const call = cToken.methods.repayBorrow();

            if (
              // If they are repaying their whole balance:
              amountBN.toString() === (await fuse.web3.eth.getBalance(address))
            ) {
              // Subtract gas for max ETH

              const { gasWEI, gasPrice, estimatedGas } = await fetchGasForCall(
                call,
                amountBN,
                fuse,
                address
              );

              await call.send({
                from: address,
                value: amountBN.sub(gasWEI),

                gasPrice,
                gas: estimatedGas,
              });
            } else {
              await call.send({
                from: address,
                value: amountBN,
              });
            }
          } else {
            await testForCTokenErrorAndSend(
              cToken.methods.repayBorrow(isRepayingMax ? max : amountBN),
              address,
              "Cannot repay this amount right now!"
            );
          }

          LogRocket.track("Fuse-Repay");
        }
      } else if (mode === Mode.BORROW) {
        await testForCTokenErrorAndSend(
          cToken.methods.borrow(amountBN),
          address,
          "Cannot borrow this amount right now!"
        );

        LogRocket.track("Fuse-Borrow");
      } else if (mode === Mode.WITHDRAW) {
        await testForCTokenErrorAndSend(
          cToken.methods.redeemUnderlying(amountBN),
          address,
          "Cannot withdraw this amount right now!"
        );

        LogRocket.track("Fuse-Withdraw");
      }

      queryClient.refetchQueries();

      // Wait 2 seconds for refetch and then close modal.
      // We do this instead of waiting the refetch because some refetches take a while or error out and we want to close now.
      await new Promise((resolve) => setTimeout(resolve, 2000));

      onClose();
    } catch (e) {
      handleGenericError(e, toast);
      setUserAction(UserAction.NO_ACTION);
    }
  };

  const symbol = getSymbol(tokenData, asset);

  return (
    <Column
      mainAxisAlignment="flex-start"
      crossAxisAlignment="flex-start"
      height={showEnableAsCollateral ? "575px" : "500px"}
    >
      {userAction === UserAction.WAITING_FOR_TRANSACTIONS ? (
        <Column
          expand
          mainAxisAlignment="center"
          crossAxisAlignment="center"
          p={4}
        >
          <HashLoader size={70} color={tokenData?.color ?? "#FFF"} loading />
          <Heading mt="30px" textAlign="center" size="md">
            {t("Check your wallet to submit the transactions")}
          </Heading>
          <Text fontSize="sm" mt="15px" textAlign="center">
            {t("Do not close this tab until you submit all transactions!")}
          </Text>
        </Column>
      ) : (
        <>
          <Row
            width="100%"
            mainAxisAlignment="center"
            crossAxisAlignment="center"
            p={4}
            height="72px"
            flexShrink={0}
          >
            <Box height="35px" width="35px">
              <Image
                width="100%"
                height="100%"
                borderRadius="50%"
                src={
                  tokenData?.logoURL ??
                  "https://raw.githubusercontent.com/feathericons/feather/master/icons/help-circle.svg"
                }
                alt=""
              />
            </Box>

            <Heading fontSize="27px" ml={3}>
              {!isMobile && asset.underlyingName.length < 25
                ? asset.underlyingName
                : symbol}
            </Heading>
          </Row>

          <ModalDivider />

          <Column
            mainAxisAlignment="flex-start"
            crossAxisAlignment="center"
            px={4}
            pb={4}
            pt={1}
            height="100%"
          >
            <Column
              mainAxisAlignment="flex-start"
              crossAxisAlignment="flex-start"
              width="100%"
            >
              <TabBar color={tokenData?.color} mode={mode} setMode={setMode} />

              <DashboardBox width="100%" height="70px">
                <Row
                  p={4}
                  mainAxisAlignment="space-between"
                  crossAxisAlignment="center"
                  expand
                >
                  <AmountInput
                    color={tokenData?.color ?? "#FFF"}
                    displayAmount={userEnteredAmount}
                    updateAmount={updateAmount}
                    disabled={mode === Mode.BORROW && isBorrowPaused}
                  />
                  <TokenNameAndMaxButton
                    comptrollerAddress={comptrollerAddress}
                    mode={mode}
                    symbol={symbol}
                    logoURL={
                      tokenData?.logoURL ??
                      "https://raw.githubusercontent.com/feathericons/feather/master/icons/help-circle.svg"
                    }
                    asset={asset}
                    updateAmount={updateAmount}
                  />
                </Row>
              </DashboardBox>
            </Column>

            <StatsColumn
              symbol={symbol}
              amount={parseInt(amount?.toFixed(0) ?? "0") ?? 0}
              color={tokenData?.color ?? "#FFF"}
              assets={assets}
              index={index}
              mode={mode}
              enableAsCollateral={enableAsCollateral}
            />

            {showEnableAsCollateral ? (
              <DashboardBox p={4} width="100%" mt={4}>
                <Row
                  mainAxisAlignment="space-between"
                  crossAxisAlignment="center"
                  width="100%"
                >
                  <Text fontWeight="bold">{t("Enable As Collateral")}:</Text>
                  <SwitchCSS
                    symbol={asset.underlyingSymbol}
                    color={tokenData?.color}
                  />
                  <Switch
                    h="20px"
                    className={asset.underlyingSymbol + "-switch"}
                    isChecked={enableAsCollateral}
                    onChange={() => {
                      setEnableAsCollateral((past) => !past);
                    }}
                  />
                </Row>
              </DashboardBox>
            ) : null}

            <Button
              mt={4}
              fontWeight="bold"
              fontSize={
                depositOrWithdrawAlert ? depositOrWithdrawAlertFontSize : "2xl"
              }
              borderRadius="10px"
              width="100%"
              height="70px"
              bg={tokenData?.color ?? "#FFF"}
              color={tokenData?.overlayTextColor ?? "#000"}
              // If the size is small, this means the text is large and we don't want the font size scale animation.
              className={
                isMobile ||
                  depositOrWithdrawAlertFontSize === "14px" ||
                  depositOrWithdrawAlertFontSize === "15px"
                  ? "confirm-button-disable-font-size-scale"
                  : ""
              }
              _hover={{ transform: "scale(1.02)" }}
              _active={{ transform: "scale(0.95)" }}
              onClick={onConfirm}
              isDisabled={!amountIsValid}
            >
              {depositOrWithdrawAlert ?? t("Confirm")}
            </Button>
          </Column>
        </>
      )}
    </Column>
  );
}
Example #19
Source File: EditRewardsDistributorModal.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
EditRewardsDistributorModal = ({
  rewardsDistributor,
  pool,
  isOpen,
  onClose,
}: {
  rewardsDistributor: RewardsDistributor;
  pool: FusePoolData;
  isOpen: boolean;
  onClose: () => any;
}) => {
  const { t } = useTranslation();

  const { address, fuse } = useRari();
  const rewardsDistributorInstance = useRewardsDistributorInstance(
    rewardsDistributor.address
  );
  const tokenData = useTokenData(rewardsDistributor.rewardToken);
  const isAdmin = address === rewardsDistributor.admin;

  //   Balances
  const { data: balanceERC20 } = useTokenBalance(
    rewardsDistributor.rewardToken,
    rewardsDistributor.address
  );

  const { data: myBalance } = useTokenBalance(rewardsDistributor.rewardToken);

  const toast = useToast();

  // Inputs
  const [sendAmt, setSendAmt] = useState<number>(0);

  const [supplySpeed, setSupplySpeed] = useState<number>(0.001);
  const [borrowSpeed, setBorrowSpeed] = useState<number>(0.001);

  //  Loading states
  const [fundingDistributor, setFundingDistributor] = useState(false);
  const [seizing, setSeizing] = useState(false);
  const [changingSpeed, setChangingSpeed] = useState(false);
  const [changingBorrowSpeed, setChangingBorrowSpeed] = useState(false);
  const [selectedAsset, setSelectedAsset] = useState<
    USDPricedFuseAsset | undefined
  >(pool?.assets[0] ?? undefined);

  //   RewardsSpeeds
  const [supplySpeedForCToken, borrowSpeedForCToken] = useRewardSpeedsOfCToken(
    rewardsDistributor.address,
    selectedAsset?.cToken
  );

  const { hasCopied, onCopy } = useClipboard(rewardsDistributor?.address ?? "");

  // Sends tokens to distributor
  const fundDistributor = async () => {
    // Create ERC20 instance of rewardToken
    const token = new fuse.web3.eth.Contract(
      JSON.parse(
        fuse.compoundContracts["contracts/EIP20Interface.sol:EIP20Interface"]
          .abi
      ),
      rewardsDistributor.rewardToken
    );

    setFundingDistributor(true);
    try {
      await token.methods
        .transfer(
          rewardsDistributor.address,
          Fuse.Web3.utils
            .toBN(sendAmt)
            .mul(
              Fuse.Web3.utils
                .toBN(10)
                .pow(Fuse.Web3.utils.toBN(tokenData?.decimals ?? 18))
            )
        )
        .send({
          from: address,
        });

      setFundingDistributor(false);
    } catch (err) {
      handleGenericError(err, toast);
      setFundingDistributor(false);
    }
  };

  //   Adds LM to supply side of a CToken in this fuse pool
  const changeSupplySpeed = async () => {
    try {
      if (!isAdmin) throw new Error("User is not admin of this Distributor!");

      setChangingSpeed(true);

      await rewardsDistributorInstance.methods
        ._setCompSupplySpeed(
          selectedAsset?.cToken,
          Fuse.Web3.utils.toBN(supplySpeed * 10 ** (tokenData?.decimals ?? 18)) // set supplySpeed to 0.001e18 for now
        )
        .send({ from: address });

      setChangingSpeed(false);
    } catch (err) {
      handleGenericError(err, toast);
      setChangingSpeed(false);
    }
  };

  //   Adds LM to supply side of a CToken in this fuse pool
  const changeBorrowSpeed = async () => {
    try {
      if (!isAdmin) throw new Error("User is not admin of this Distributor!");

      setChangingBorrowSpeed(true);

      await rewardsDistributorInstance.methods
        ._setCompBorrowSpeed(
          selectedAsset?.cToken,
          Fuse.Web3.utils.toBN(borrowSpeed * 10 ** (tokenData?.decimals ?? 18)) // set supplySpeed to 0.001e18 for now
        )
        .send({ from: address });

      setChangingBorrowSpeed(false);
    } catch (err) {
      handleGenericError(err, toast);
      setChangingBorrowSpeed(false);
    }
  };

  const handleSeizeTokens = async () => {
    setSeizing(true);
    if (isAdmin) {
      await rewardsDistributorInstance.methods._grantComp(
        address,
        balanceERC20
      );
    } else {
      toast({
        title: "Admin Only!",
        description: "Only admin can seize tokens!",
        status: "error",
        duration: 9000,
        isClosable: true,
        position: "top-right",
      });
    }
    setSeizing(false);
  };

  return (
    <Modal
      motionPreset="slideInBottom"
      isOpen={isOpen}
      onClose={onClose}
      isCentered
    >
      <ModalOverlay />
      <ModalContent {...MODAL_PROPS}>
        <Heading fontSize="27px" my={4} textAlign="center">
          {t("Edit Rewards Distributor")}
        </Heading>

        <ModalDivider />

        {/*  RewardToken data */}
        <Column
          mainAxisAlignment="flex-start"
          crossAxisAlignment="center"
          p={4}
        >
          <>
            {tokenData?.logoURL ? (
              <Image
                mt={4}
                src={tokenData.logoURL}
                boxSize="50px"
                borderRadius="50%"
                backgroundImage={`url(${SmallWhiteCircle})`}
                backgroundSize="100% auto"
              />
            ) : null}
            <Heading
              my={tokenData?.symbol ? 3 : 6}
              fontSize="22px"
              color={tokenData?.color ?? "#FFF"}
            >
              {tokenData ? tokenData.name ?? "Invalid Address!" : "Loading..."}
            </Heading>
            <Text>
              {balanceERC20 && tokenData && tokenData.decimals
                ? (
                    parseFloat(balanceERC20?.toString()) /
                    10 ** tokenData.decimals
                  ).toFixed(3)
                : 0}{" "}
              {tokenData?.symbol}
            </Text>
            <Text onClick={onCopy}>
              Contract: {shortAddress(rewardsDistributor.address)}{" "}
              {hasCopied && "Copied!"}
            </Text>
          </>
        </Column>

        <AdminAlert
          isAdmin={isAdmin}
          mt={2}
          isNotAdminText="You are not the admin of this RewardsDistributor. Only the admin can configure rewards."
        />

        {/* Basic Info  */}
        <Column
          mainAxisAlignment="flex-start"
          crossAxisAlignment="flex-start"
          py={4}
        >
          {/* <Row mainAxisAlignment="flex-start" crossAxisAlignment="center">
            <Text>Address: {rewardsDistributor.address}</Text>
          </Row>
          <Row mainAxisAlignment="flex-start" crossAxisAlignment="center">
            <Text>Admin: {rewardsDistributor.admin}</Text>
          </Row>
          <Row mainAxisAlignment="flex-start" crossAxisAlignment="center">
            <Text>
              Balance:{" "}
              {balanceERC20 ? parseFloat(balanceERC20?.toString()) / 1e18 : 0}{" "}
              {tokenData?.symbol}
            </Text>
          </Row> */}

          <ModalDivider />

          {/* Fund distributor */}
          <Column
            mainAxisAlignment="flex-start"
            crossAxisAlignment="flex-start"
            p={4}
          >
            <Heading fontSize={"lg"}> Fund Distributor </Heading>
            <Row
              mainAxisAlignment="flex-start"
              crossAxisAlignment="center"
              mt={1}
            >
              <NumberInput
                step={0.1}
                min={0}
                onChange={(valueString) => {
                  console.log({ valueString });
                  setSendAmt(parseFloat(valueString));
                }}
              >
                <NumberInputField
                  width="100%"
                  textAlign="center"
                  placeholder={"0 " + tokenData?.symbol}
                />
                <NumberInputStepper>
                  <NumberIncrementStepper />
                  <NumberDecrementStepper />
                </NumberInputStepper>
              </NumberInput>
              <Button
                onClick={fundDistributor}
                bg="black"
                disabled={fundingDistributor}
              >
                {fundingDistributor ? <Spinner /> : "Send"}
              </Button>
              {isAdmin && (!balanceERC20?.isZero() ?? false) && (
                <Button onClick={handleSeizeTokens} bg="red" disabled={seizing}>
                  {seizing ? <Spinner /> : "Withdraw Tokens"}
                </Button>
              )}
            </Row>
            <Text mt={1}>
              Your balance:{" "}
              {myBalance
                ? (
                    parseFloat(myBalance?.toString()) /
                    10 ** (tokenData?.decimals ?? 18)
                  ).toFixed(2)
                : 0}{" "}
              {tokenData?.symbol}
            </Text>
          </Column>

          {/* Add or Edit a CToken to the Distributor */}

          {pool.assets.length ? (
            <Column
              mainAxisAlignment="flex-start"
              crossAxisAlignment="flex-start"
              p={4}
            >
              <Heading fontSize={"lg"}> Manage CToken Rewards </Heading>
              {/* Select Asset */}
              <Row
                mainAxisAlignment="flex-start"
                crossAxisAlignment="center"
                mt={1}
              >
                {pool.assets.map(
                  (asset: USDPricedFuseAsset, index: number, array: any[]) => {
                    return (
                      <Box
                        pr={index === array.length - 1 ? 4 : 2}
                        key={asset.cToken}
                        flexShrink={0}
                      >
                        <DashboardBox
                          as="button"
                          onClick={() => setSelectedAsset(asset)}
                          {...(asset.cToken === selectedAsset?.cToken
                            ? activeStyle
                            : noop)}
                        >
                          <Center expand px={4} py={1} fontWeight="bold">
                            {asset.underlyingSymbol}
                          </Center>
                        </DashboardBox>
                      </Box>
                    );
                  }
                )}
              </Row>

              {/* Change Supply Speed */}
              <Column
                mainAxisAlignment="flex-start"
                crossAxisAlignment="flex-start"
                py={3}
              >
                <Row
                  mainAxisAlignment="flex-start"
                  crossAxisAlignment="flex-start"
                >
                  <NumberInput
                    step={0.1}
                    min={0}
                    onChange={(supplySpeed) => {
                      console.log({ supplySpeed });
                      setSupplySpeed(parseFloat(supplySpeed));
                    }}
                  >
                    <NumberInputField
                      width="100%"
                      textAlign="center"
                      placeholder={"0 " + tokenData?.symbol}
                    />
                    <NumberInputStepper>
                      <NumberIncrementStepper />
                      <NumberDecrementStepper />
                    </NumberInputStepper>
                  </NumberInput>
                  <Button
                    onClick={changeSupplySpeed}
                    bg="black"
                    disabled={changingSpeed || !isAdmin}
                  >
                    {changingSpeed ? <Spinner /> : "Set"}
                  </Button>
                </Row>
                <Row
                  mainAxisAlignment="flex-start"
                  crossAxisAlignment="flex-start"
                >
                  <Text>
                    Supply Speed:{" "}
                    {(parseFloat(supplySpeedForCToken) / 1e18).toFixed(4)}
                  </Text>
                </Row>
              </Column>

              {/* Change Borrow Speed */}
              <Column
                mainAxisAlignment="flex-start"
                crossAxisAlignment="flex-start"
                py={3}
              >
                <Row
                  mainAxisAlignment="flex-start"
                  crossAxisAlignment="flex-start"
                >
                  <NumberInput
                    step={0.1}
                    min={0}
                    onChange={(borrowSpeed) => {
                      console.log({ borrowSpeed });
                      setBorrowSpeed(parseFloat(borrowSpeed));
                    }}
                  >
                    <NumberInputField
                      width="100%"
                      textAlign="center"
                      placeholder={"0 " + tokenData?.symbol}
                    />
                    <NumberInputStepper>
                      <NumberIncrementStepper />
                      <NumberDecrementStepper />
                    </NumberInputStepper>
                  </NumberInput>

                  <Button
                    onClick={changeBorrowSpeed}
                    bg="black"
                    disabled={changingBorrowSpeed || !isAdmin}
                  >
                    {changingBorrowSpeed ? <Spinner /> : "Set"}
                  </Button>
                </Row>
                <Row
                  mainAxisAlignment="flex-start"
                  crossAxisAlignment="flex-start"
                >
                  <Text>
                    Borrow Speed:{" "}
                    {(parseFloat(borrowSpeedForCToken) / 1e18).toFixed(2)}
                  </Text>
                </Row>
              </Column>
            </Column>
          ) : (
            <Center p={4}>
              <Text fontWeight="bold">
                Add CTokens to this pool to configure their rewards.
              </Text>
            </Center>
          )}
        </Column>
      </ModalContent>
    </Modal>
  );
}
Example #20
Source File: AddRewardsDistributorModal.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AddRewardsDistributorModal = ({
  comptrollerAddress,
  poolName,
  poolID,
  isOpen,
  onClose,
}: {
  comptrollerAddress: string;
  poolName: string;
  poolID: string;
  isOpen: boolean;
  onClose: () => any;
}) => {
  const { fuse, address: userAddress } = useRari();
  const { t } = useTranslation();
  const toast = useToast();

  const [isDeploying, setIsDeploying] = useState(false);

  const [address, setAddress] = useState<string>(
    ""
  );
  const [rewardToken, setRewardToken] = useState<string>("");

  // If you have selected "Add RewardsDistributor, this value will be filled"
  const [nav, setNav] = useState<Nav>(Nav.CREATE);

  // Stepper
  const [activeStep, setActiveStep] = useState<0 | 1 | 2>(0);

  const tokenData = useTokenData(rewardToken);

  const isEmpty = address.trim() === "";

  useEffect(() => {
    const isRewardsDistributorAddress = nav === Nav.ADD;
    if (isRewardsDistributorAddress) {
      setRewardToken("");
    }

    try {
      const validAddress = Web3.utils.toChecksumAddress(address);
      if (validAddress && isRewardsDistributorAddress) {
        const rd = createRewardsDistributor(address, fuse);
        rd.methods
          .rewardToken()
          .call()
          .then((tokenAddr: string) => setRewardToken(tokenAddr));
      }

      // If we're creating a rewardsDistributor then this is the rewardToken
      if (validAddress && !isRewardsDistributorAddress) {
        setRewardToken(address);
      }
    } catch (err) {
      return;
    }

    // If we're adding a rewardsDistributor then get the rewardToken
  }, [fuse, address, nav]);

  const handleDeploy = async () => {
    if (!tokenData) return;
    setIsDeploying(true);

    let rDAddress = address;
    try {
      if (nav === Nav.CREATE) {
        rDAddress = await deploy();
      }
    } catch (err) {
      console.log({ err });
      setIsDeploying(false);
      toast({
        title: "Error deploying RewardsDistributor",
        description: "",
        status: "error",
        duration: 10000,
        isClosable: true,
        position: "top-right",
      });
      return;
    }

    setActiveStep(1);

    try {
      await addRDToComptroller(comptrollerAddress, rDAddress, fuse);

      setIsDeploying(false);
      onClose();
    } catch (err) {
      console.log({ err });
      setIsDeploying(false);
      toast({
        title: "Error adding RewardsDistributor to Comptroller",
        description: "",
        status: "error",
        duration: 10000,
        isClosable: true,
        position: "top-right",
      });
      return;
    }
  };

  // Deploy new RD
  const deploy = async (): Promise<string> => {
    if (!tokenData) throw new Error("No tokendata ");

    const deployedDistributor = await fuse.deployRewardsDistributor(
      tokenData.address,
      {
        from: userAddress,
      }
    );

    toast({
      title: "RewardsDistributor Deployed",
      description: "RewardsDistributor for " + tokenData.symbol + " deployed",
      status: "success",
      duration: 2000,
      isClosable: true,
      position: "top-right",
    });

    const rDAddress = deployedDistributor.options.address;
    return rDAddress;
  };

  const addRDToComptroller = async (
    comptrollerAddress: string,
    rDAddress: string,
    fuse: Fuse
  ) => {
    const comptroller = await createComptroller(comptrollerAddress, fuse);

    if (!comptroller || !comptroller.methods._addRewardsDistributor) {
      throw new Error("Could not create Comptroller");
    }

    // Add distributor to pool Comptroller
    await comptroller.methods
      ._addRewardsDistributor(rDAddress)
      .send({ from: userAddress });

    toast({
      title: "RewardsDistributor Added to Pool",
      description: "",
      status: "success",
      duration: 2000,
      isClosable: true,
      position: "top-right",
    });
  };

  const subtitle = useMemo(() => {
    if (nav === Nav.CREATE) {
      return tokenData
        ? tokenData.name ?? "Invalid ERC20 Token Address!"
        : "Loading...";
    } else {
      return tokenData
        ? tokenData.name ?? "Invalid RewardsDistributor Address!"
        : "Loading...";
    }
  }, [tokenData, nav]);

  return (
    <Modal
      motionPreset="slideInBottom"
      isOpen={isOpen}
      onClose={onClose}
      isCentered
      size="lg"
    >
      <ModalOverlay />
      <ModalContent {...MODAL_PROPS}>
        <Heading fontSize="27px" my={4} textAlign="center">
          {nav === Nav.CREATE
            ? t("Deploy Rewards Distributor")
            : t("Add Rewards Distributor")}
        </Heading>

        <ModalDivider />

        <Box h="100%" w="100%" bg="">
          <Row
            mainAxisAlignment="flex-start"
            crossAxisAlignment="flex-start"
            bg=""
            p={4}
          >
            <RadioGroup onChange={(value: Nav) => setNav(value)} value={nav}>
              <Stack direction="row">
                <Radio value={Nav.CREATE} disabled={isDeploying}>
                  Create
                </Radio>
                <Radio value={Nav.ADD} disabled={isDeploying}>
                  Add
                </Radio>
              </Stack>
            </RadioGroup>
          </Row>

          <Column
            mainAxisAlignment="flex-start"
            crossAxisAlignment="center"
            pb={4}
          >
            {!isEmpty ? (
              <>
                {tokenData?.logoURL ? (
                  <Image
                    mt={4}
                    src={tokenData.logoURL}
                    boxSize="50px"
                    borderRadius="50%"
                    backgroundImage={`url(${SmallWhiteCircle})`}
                    backgroundSize="100% auto"
                  />
                ) : null}
                <Heading
                  my={tokenData?.symbol ? 3 : 6}
                  fontSize="22px"
                  color={tokenData?.color ?? "#FFF"}
                >
                  {subtitle}
                </Heading>
              </>
            ) : null}

            <Center px={4} mt={isEmpty ? 4 : 0} width="100%">
              <Input
                width="100%"
                textAlign="center"
                placeholder={
                  nav === Nav.CREATE
                    ? t(
                        "Token Address: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
                      )
                    : t("RewardsDistributor Address:")
                }
                height="40px"
                variant="filled"
                size="sm"
                value={address}
                onChange={(event) => setAddress(event.target.value)}
                {...DASHBOARD_BOX_PROPS}
                _placeholder={{ color: "#e0e0e0" }}
                _focus={{ bg: "#121212" }}
                _hover={{ bg: "#282727" }}
                bg="#282727"
              />
            </Center>

            {isDeploying && (
              <Box my={3} w="100%" h="100%">
                <TransactionStepper
                  activeStep={activeStep}
                  tokenData={tokenData}
                  steps={steps}
                />
              </Box>
            )}

            {tokenData?.symbol && (
              <Box px={4} mt={4} width="100%">
                <Button
                  fontWeight="bold"
                  fontSize="2xl"
                  borderRadius="10px"
                  width="100%"
                  height="70px"
                  color={tokenData.overlayTextColor! ?? "#000"}
                  bg={tokenData.color! ?? "#FFF"}
                  _hover={{ transform: "scale(1.02)" }}
                  _active={{ transform: "scale(0.95)" }}
                  // isLoading={isDeploying}
                  disabled={isDeploying}
                  onClick={handleDeploy}
                >
                  {isDeploying
                    ? steps[activeStep]
                    : nav === Nav.CREATE
                    ? t("Deploy RewardsDistributor")
                    : t("Add RewardsDistributor")}
                </Button>
              </Box>
            )}
          </Column>
        </Box>
      </ModalContent>
    </Modal>
  );
}
Example #21
Source File: AddAssetModal.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AssetSettings = ({
  poolName,
  poolID,
  tokenData,
  comptrollerAddress,
  cTokenAddress,
  existingAssets,
  closeModal,
}: {
  poolName: string;
  poolID: string;
  comptrollerAddress: string;
  tokenData: TokenData;

  // Only for editing mode
  cTokenAddress?: string;

  // Only for add asset modal
  existingAssets?: USDPricedFuseAsset[];
  closeModal: () => any;
}) => {
  const { t } = useTranslation();
  const { fuse, address } = useRari();
  const toast = useToast();
  const queryClient = useQueryClient();

  const [isDeploying, setIsDeploying] = useState(false);

  const [collateralFactor, setCollateralFactor] = useState(50);
  const [reserveFactor, setReserveFactor] = useState(10);
  const [adminFee, setAdminFee] = useState(0);

  const [isBorrowPaused, setIsBorrowPaused] = useState(false);

  const scaleCollateralFactor = (_collateralFactor: number) => {
    return _collateralFactor / 1e16;
  };

  const scaleReserveFactor = (_reserveFactor: number) => {
    return _reserveFactor / 1e16;
  };

  const scaleAdminFee = (_adminFee: number) => {
    return _adminFee / 1e16;
  };

  const [interestRateModel, setInterestRateModel] = useState(
    Fuse.PUBLIC_INTEREST_RATE_MODEL_CONTRACT_ADDRESSES
      .JumpRateModel_Cream_Stables_Majors
  );

  const { data: curves } = useQuery(
    interestRateModel + adminFee + reserveFactor + " irm",
    async () => {
      const IRM = await fuse.identifyInterestRateModel(interestRateModel);

      if (IRM === null) {
        return null;
      }

      await IRM._init(
        fuse.web3,
        interestRateModel,
        // reserve factor
        reserveFactor * 1e16,
        // admin fee
        adminFee * 1e16,
        // hardcoded 10% Fuse fee
        0.1e18
      );

      return convertIRMtoCurve(IRM, fuse);
    }
  );

  const deploy = async () => {
    // If pool already contains this asset:
    if (
      existingAssets!.some(
        (asset) => asset.underlyingToken === tokenData.address
      )
    ) {
      toast({
        title: "Error!",
        description: "You have already added this asset to this pool.",
        status: "error",
        duration: 2000,
        isClosable: true,
        position: "top-right",
      });

      return;
    }

    setIsDeploying(true);

    // 50% -> 0.5 * 1e18
    const bigCollateralFacotr = new BigNumber(collateralFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    // 10% -> 0.1 * 1e18
    const bigReserveFactor = new BigNumber(reserveFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    // 5% -> 0.05 * 1e18
    const bigAdminFee = new BigNumber(adminFee)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    const conf: any = {
      underlying: tokenData.address,
      comptroller: comptrollerAddress,
      interestRateModel,
      initialExchangeRateMantissa: fuse.web3.utils.toBN(1e18),

      // Ex: BOGGED USDC
      name: poolName + " " + tokenData.name,
      // Ex: fUSDC-456
      symbol: "f" + tokenData.symbol + "-" + poolID,
      decimals: 8,
    };

    try {
      await fuse.deployAsset(
        conf,
        bigCollateralFacotr,
        bigReserveFactor,
        bigAdminFee,
        { from: address },
        // TODO: Disable this. This bypasses the price feed check. Only using now because only trusted partners are deploying assets.
        true
      );

      LogRocket.track("Fuse-DeployAsset");

      queryClient.refetchQueries();
      // Wait 2 seconds for refetch and then close modal.
      // We do this instead of waiting the refetch because some refetches take a while or error out and we want to close now.
      await new Promise((resolve) => setTimeout(resolve, 2000));

      toast({
        title: "You have successfully added an asset to this pool!",
        description: "You may now lend and borrow with this asset.",
        status: "success",
        duration: 2000,
        isClosable: true,
        position: "top-right",
      });

      closeModal();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  const liquidationIncentiveMantissa =
    useLiquidationIncentive(comptrollerAddress);

  const cTokenData = useCTokenData(comptrollerAddress, cTokenAddress);

  // Update values on refetch!
  useEffect(() => {
    if (cTokenData) {
      setCollateralFactor(cTokenData.collateralFactorMantissa / 1e16);
      setReserveFactor(cTokenData.reserveFactorMantissa / 1e16);
      setAdminFee(cTokenData.adminFeeMantissa / 1e16);
      setInterestRateModel(cTokenData.interestRateModelAddress);
      setIsBorrowPaused(cTokenData.isPaused);
    }
  }, [cTokenData]);

  const togglePause = async () => {
    const comptroller = createComptroller(comptrollerAddress, fuse);

    try {
      await comptroller.methods
        ._setBorrowPaused(cTokenAddress, !isBorrowPaused)
        .send({ from: address });

      LogRocket.track("Fuse-PauseToggle");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  const updateCollateralFactor = async () => {
    const comptroller = createComptroller(comptrollerAddress, fuse);

    // 70% -> 0.7 * 1e18
    const bigCollateralFactor = new BigNumber(collateralFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    try {
      await testForComptrollerErrorAndSend(
        comptroller.methods._setCollateralFactor(
          cTokenAddress,
          bigCollateralFactor
        ),
        address,
        ""
      );

      LogRocket.track("Fuse-UpdateCollateralFactor");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  const updateReserveFactor = async () => {
    const cToken = createCToken(fuse, cTokenAddress!);

    // 10% -> 0.1 * 1e18
    const bigReserveFactor = new BigNumber(reserveFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);
    try {
      await testForCTokenErrorAndSend(
        cToken.methods._setReserveFactor(bigReserveFactor),
        address,
        ""
      );

      LogRocket.track("Fuse-UpdateReserveFactor");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  const updateAdminFee = async () => {
    const cToken = createCToken(fuse, cTokenAddress!);

    // 5% -> 0.05 * 1e18
    const bigAdminFee = new BigNumber(adminFee)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    try {
      await testForCTokenErrorAndSend(
        cToken.methods._setAdminFee(bigAdminFee),
        address,
        ""
      );

      LogRocket.track("Fuse-UpdateAdminFee");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  const updateInterestRateModel = async () => {
    const cToken = createCToken(fuse, cTokenAddress!);

    try {
      await testForCTokenErrorAndSend(
        cToken.methods._setInterestRateModel(interestRateModel),
        address,
        ""
      );

      LogRocket.track("Fuse-UpdateInterestRateModel");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  return (
    cTokenAddress ? cTokenData?.cTokenAddress === cTokenAddress : true
  ) ? (
    <Column
      mainAxisAlignment="flex-start"
      crossAxisAlignment="flex-start"
      overflowY="auto"
      width="100%"
      height="100%"
    >
      <ConfigRow height="35px">
        <SimpleTooltip
          label={t(
            "Collateral factor can range from 0-90%, and represents the proportionate increase in liquidity (borrow limit) that an account receives by depositing the asset."
          )}
        >
          <Text fontWeight="bold">
            {t("Collateral Factor")} <QuestionIcon ml={1} mb="4px" />
          </Text>
        </SimpleTooltip>

        {cTokenData &&
        collateralFactor !==
          scaleCollateralFactor(cTokenData.collateralFactorMantissa) ? (
          <SaveButton ml={3} onClick={updateCollateralFactor} />
        ) : null}

        <SliderWithLabel
          ml="auto"
          value={collateralFactor}
          setValue={setCollateralFactor}
          formatValue={formatPercentage}
          step={0.5}
          max={
            liquidationIncentiveMantissa
              ? // 100% CF - Liquidation Incentive (ie: 8%) - 5% buffer
                100 - (liquidationIncentiveMantissa.toString() / 1e16 - 100) - 5
              : 90
          }
        />
      </ConfigRow>

      <ModalDivider />
      {cTokenAddress ? (
        <ConfigRow>
          <SimpleTooltip
            label={t("If enabled borrowing this asset will be disabled.")}
          >
            <Text fontWeight="bold">
              {t("Pause Borrowing")} <QuestionIcon ml={1} mb="4px" />
            </Text>
          </SimpleTooltip>

          <SaveButton
            ml="auto"
            onClick={togglePause}
            fontSize="xs"
            altText={
              isBorrowPaused ? t("Enable Borrowing") : t("Pause Borrowing")
            }
          />
        </ConfigRow>
      ) : null}

      <ModalDivider />

      <ConfigRow height="35px">
        <SimpleTooltip
          label={t(
            "The fraction of interest generated on a given asset that is routed to the asset's Reserve Pool. The Reserve Pool protects lenders against borrower default and liquidation malfunction."
          )}
        >
          <Text fontWeight="bold">
            {t("Reserve Factor")} <QuestionIcon ml={1} mb="4px" />
          </Text>
        </SimpleTooltip>

        {cTokenData &&
        reserveFactor !==
          scaleReserveFactor(cTokenData.reserveFactorMantissa) ? (
          <SaveButton ml={3} onClick={updateReserveFactor} />
        ) : null}

        <SliderWithLabel
          ml="auto"
          value={reserveFactor}
          setValue={setReserveFactor}
          formatValue={formatPercentage}
          max={50}
        />
      </ConfigRow>
      <ModalDivider />

      <ConfigRow height="35px">
        <SimpleTooltip
          label={t(
            "The fraction of interest generated on a given asset that is routed to the asset's admin address as a fee."
          )}
        >
          <Text fontWeight="bold">
            {t("Admin Fee")} <QuestionIcon ml={1} mb="4px" />
          </Text>
        </SimpleTooltip>

        {cTokenData &&
        adminFee !== scaleAdminFee(cTokenData.adminFeeMantissa) ? (
          <SaveButton ml={3} onClick={updateAdminFee} />
        ) : null}

        <SliderWithLabel
          ml="auto"
          value={adminFee}
          setValue={setAdminFee}
          formatValue={formatPercentage}
          max={30}
        />
      </ConfigRow>

      <ModalDivider />

      <ConfigRow>
        <SimpleTooltip
          label={t(
            "The interest rate model chosen for an asset defines the rates of interest for borrowers and suppliers at different utilization levels."
          )}
        >
          <Text fontWeight="bold">
            {t("Interest Model")} <QuestionIcon ml={1} mb="4px" />
          </Text>
        </SimpleTooltip>

        <Select
          {...DASHBOARD_BOX_PROPS}
          ml="auto"
          borderRadius="7px"
          fontWeight="bold"
          _focus={{ outline: "none" }}
          width="260px"
          value={interestRateModel.toLowerCase()}
          onChange={(event) => setInterestRateModel(event.target.value)}
        >
          {Object.entries(
            Fuse.PUBLIC_INTEREST_RATE_MODEL_CONTRACT_ADDRESSES
          ).map(([key, value]) => {
            return (
              <option
                className="black-bg-option"
                value={value.toLowerCase()}
                key={key}
              >
                {key}
              </option>
            );
          })}
        </Select>

        {cTokenData &&
        cTokenData.interestRateModelAddress.toLowerCase() !==
          interestRateModel.toLowerCase() ? (
          <SaveButton
            height="40px"
            borderRadius="7px"
            onClick={updateInterestRateModel}
          />
        ) : null}
      </ConfigRow>

      <Box
        height="170px"
        width="100%"
        color="#000000"
        overflow="hidden"
        pl={2}
        pr={3}
        className="hide-bottom-tooltip"
        flexShrink={0}
      >
        {curves ? (
          <Chart
            options={
              {
                ...FuseIRMDemoChartOptions,
                colors: ["#FFFFFF", tokenData.color! ?? "#282727"],
              } as any
            }
            type="line"
            width="100%"
            height="100%"
            series={[
              {
                name: "Borrow Rate",
                data: curves.borrowerRates,
              },
              {
                name: "Deposit Rate",
                data: curves.supplierRates,
              },
            ]}
          />
        ) : curves === undefined ? (
          <Center expand color="#FFF">
            <Spinner my={8} />
          </Center>
        ) : (
          <Center expand color="#FFFFFF">
            <Text>
              {t("No graph is available for this asset's interest curves.")}
            </Text>
          </Center>
        )}
      </Box>

      {cTokenAddress ? null : (
        <Box px={4} mt={4} width="100%">
          <Button
            fontWeight="bold"
            fontSize="2xl"
            borderRadius="10px"
            width="100%"
            height="70px"
            color={tokenData.overlayTextColor! ?? "#000"}
            bg={tokenData.color! ?? "#FFF"}
            _hover={{ transform: "scale(1.02)" }}
            _active={{ transform: "scale(0.95)" }}
            isLoading={isDeploying}
            onClick={deploy}
          >
            {t("Confirm")}
          </Button>
        </Box>
      )}
    </Column>
  ) : (
    <Center expand>
      <Spinner my={8} />
    </Center>
  );
}
Example #22
Source File: OracleConfig.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
OracleConfig = () => {
  const toast = useToast();
  const { t } = useTranslation();
  const queryClient = useQueryClient();

  const { fuse, address } = useRari();

  const {
    mode,
    feeTier,
    oracleData,
    activeOracleModel,
    tokenAddress,
    oracleAddress,
    oracleTouched,
    uniV3BaseTokenAddress,
    setOracleTouched,
    activeUniSwapPair,
    setActiveOracleModel,
    setOracleAddress,
    poolOracleAddress,
    uniV3BaseTokenOracle,
    baseTokenActiveOracleName,
    shouldShowUniV3BaseTokenOracleForm,
  } = useAddAssetContext();

  const isUserAdmin = !!oracleData ? address === oracleData.admin : false;

  // Available oracle options for asset
  const options = useGetOracleOptions(oracleData, tokenAddress);

  // Identify token oracle address
  const oracleIdentity = useIdentifyOracle(oracleAddress);

  const [inputTouched, setInputTouched] = useState(false);

  // If user's editing the asset's properties, show the Ctoken's active Oracle
  useEffect(() => {
    // Map oracleIdentity to whatever the type of `activeOracle` can be

    // "Current_Price_Oracle" would only be avialable if you are editing
    if (
      mode === "Editing" &&
      options &&
      options["Current_Price_Oracle"] &&
      !oracleTouched
    ) {
      setActiveOracleModel("Current_Price_Oracle");
    }

    // if avaiable, set to "Default_Price_Oracle" if you are adding
    if (
      mode === "Adding" &&
      options &&
      !!options["Default_Price_Oracle"] &&
      !oracleTouched
    ) {
      setActiveOracleModel("Default_Price_Oracle");
    }

    // if avaiable, set to "Default_Price_Oracle" if you are adding
    if (
      mode === "Adding" &&
      options &&
      !!options["Current_Price_Oracle"] &&
      !oracleTouched
    ) {
      setActiveOracleModel("Current_Price_Oracle");
    }
  }, [
    mode,
    activeOracleModel,
    options,
    setActiveOracleModel,
    oracleIdentity,
    oracleTouched,
  ]);

  // Update the oracle address, after user chooses which option they want to use.
  // If option is Custom_Oracle or Uniswap_V3_Oracle, oracle address is changed differently so we dont trigger this.
  useEffect(() => {
    if (
      activeOracleModel.length > 0 &&
      activeOracleModel !== "Custom_Oracle" &&
      activeOracleModel !== "Uniswap_V3_Oracle" &&
      activeOracleModel !== "Uniswap_V2_Oracle" &&
      activeOracleModel !== "SushiSwap_Oracle" &&
      options
    )
      setOracleAddress(options[activeOracleModel]);
    if (
      activeUniSwapPair === "" &&
      (activeOracleModel === "Custom_Oracle" ||
        activeOracleModel === "Uniswap_V3_Oracle" ||
        activeOracleModel === "Uniswap_V2_Oracle" ||
        activeOracleModel === "SushiSwap_Oracle") &&
      !inputTouched
    )
      setOracleAddress("");
  }, [activeOracleModel, options, setOracleAddress, activeUniSwapPair]);

  // Will update oracle for the asset. This is used only if user is editing asset.
  const updateOracle = async () => {
    const poolOracleContract = createOracle(
      poolOracleAddress,
      fuse,
      "MasterPriceOracle"
    );

    // This variable will change if we deploy an oracle. (i.e TWAP Oracles)
    // If we're using an option that has been deployed it stays the same.
    let oracleAddressToUse = oracleAddress;

    try {
      if (options === null) return null;

      // If activeOracle if a TWAP Oracle
      if (activeOracleModel === "Uniswap_V3_Oracle") {
        // Check for observation cardinality and fix if necessary
        await fuse.primeUniswapV3Oracle(oracleAddressToUse, { from: address });

        // Deploy oracle
        oracleAddressToUse = await fuse.deployPriceOracle(
          "UniswapV3TwapPriceOracleV2",
          {
            feeTier,
            baseToken: uniV3BaseTokenAddress,
          },
          { from: address }
        );
      }

      const tokenArray =
        shouldShowUniV3BaseTokenOracleForm &&
        !isTokenETHOrWETH(uniV3BaseTokenAddress)
          ? [tokenAddress, uniV3BaseTokenAddress]
          : [tokenAddress];
      const oracleAddressArray =
        shouldShowUniV3BaseTokenOracleForm &&
        !isTokenETHOrWETH(uniV3BaseTokenAddress)
          ? [oracleAddressToUse, uniV3BaseTokenOracle]
          : [oracleAddressToUse];

      console.log({ tokenArray, oracleAddressArray });

      // Add oracle to Master Price Oracle
      await poolOracleContract.methods
        .add(tokenArray, oracleAddressArray)
        .send({ from: address });

      queryClient.refetchQueries();

      // Wait 2 seconds for refetch and then close modal.
      // We do this instead of waiting the refetch because some refetches take a while or error out and we want to close now.
      await new Promise((resolve) => setTimeout(resolve, 2000));

      toast({
        title: "You have successfully updated the oracle to this asset!",
        description: "Oracle will now point to the new selected address.",
        status: "success",
        duration: 2000,
        isClosable: true,
        position: "top-right",
      });
      setActiveOracleModel("Current_Price_Oracle");
      setOracleAddress(options["Current_Price_Oracle"]);
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  if (!options)
    return (
      <Center>
        <Spinner />
      </Center>
    );

  return (
    <>
      <Row
        mainAxisAlignment={mode === "Editing" ? "space-between" : "flex-start"}
        // background="gold"
        crossAxisAlignment={"center"}
        width={
          mode === "Editing"
            ? !shouldShowUniV3BaseTokenOracleForm
              ? "100%"
              : "50%"
            : "100%"
        }
        flexGrow={1}
        pt={mode === "Editing" ? 4 : 0}
        pb={mode === "Editing" ? 1 : 0}
        px={mode === "Editing" ? 4 : 0}
        id="PRICEORACLE"
      >
        <SimpleTooltip label={t("Choose the best price oracle for the asset.")}>
          <Text fontWeight="bold">
            {t("Price Oracle")} <QuestionIcon ml={1} mb="4px" />
          </Text>
        </SimpleTooltip>

        {/* Oracles */}
        <Box
          width={mode === "Editing" ? "50%" : "100%"}
          alignItems="flex-end"
          flexDirection="column"
          alignContent="center"
          display="flex"
        >
          <Select
            mb={2}
            ml="auto"
            width="260px"
            {...DASHBOARD_BOX_PROPS}
            borderRadius="7px"
            _focus={{ outline: "none" }}
            value={activeOracleModel.toLowerCase()}
            onChange={(event) => {
              // if (mode === "Editing") {
              // }
              setOracleTouched(true);
              setActiveOracleModel(event.target.value);
            }}
            placeholder={
              activeOracleModel.length === 0
                ? t("Choose Oracle")
                : activeOracleModel.replaceAll("_", " ")
            }
            disabled={
              !isUserAdmin ||
              (!oracleData?.adminOverwrite &&
                !options.Current_Price_Oracle === null)
            }
          >
            {Object.entries(options).map(([key, value]) =>
              value !== null &&
              value !== undefined &&
              key !== activeOracleModel &&
              (mode === "Adding" ? key !== "Current_Price_Oracle" : true) ? (
                // dont show the selected choice in the list
                <option key={key} value={key} className="black-bg-option">
                  {key.replaceAll("_", " ")}
                </option>
              ) : null
            )}
            {/* <option disabled={true}>Loading...</option> */}
          </Select>

          {activeOracleModel.length > 0 ? (
            <Input
              mt={2}
              mb={2}
              ml="auto"
              size="sm"
              bg="#282727"
              height="40px"
              width="260px"
              variant="filled"
              textAlign="center"
              value={oracleAddress}
              onChange={(event) => {
                const address = event.target.value;
                setInputTouched(true);
                setOracleAddress(address);
              }}
              {...DASHBOARD_BOX_PROPS}
              _focus={{ bg: "#121212" }}
              _hover={{ bg: "#282727" }}
              _placeholder={{ color: "#e0e0e0" }}
              disabled={activeOracleModel === "Custom_Oracle" ? false : true}
            />
          ) : null}
          <Text color="grey" fontSize="sm" textAlign="center">
            {oracleIdentity}
          </Text>
          {activeOracleModel === "Custom_Oracle" && (
            <Text color="red" fontSize="sm" textAlign="center">
              Make sure you know what you are doing!
            </Text>
          )}
        </Box>
      </Row>

      <Row
        mainAxisAlignment={mode === "Editing" ? "center" : "center"}
        crossAxisAlignment={mode === "Editing" ? "flex-start" : "center"}
        flexDirection="column"
        width={
          mode === "Adding" && !shouldShowUniV3BaseTokenOracleForm
            ? "100%"
            : "50%"
        }
        // bg="pink"
        ml={mode === "Editing" ? "auto" : ""}
        px={mode === "Editing" ? 4 : 0}
        id="UNIV3Config"
      >
        {activeOracleModel === "Uniswap_V3_Oracle" ? (
          <UniswapV3PriceOracleConfigurator />
        ) : null}

        {activeOracleModel === "Uniswap_V2_Oracle" ? (
          <UniswapV2OrSushiPriceOracleConfigurator type="UniswapV2" />
        ) : null}

        {activeOracleModel === "SushiSwap_Oracle" ? (
          <UniswapV2OrSushiPriceOracleConfigurator type="Sushiswap" />
        ) : null}
      </Row>

      {shouldShowUniV3BaseTokenOracleForm && mode === "Editing" ? (
        <BaseTokenOracleConfig />
      ) : null}

      {activeOracleModel !== "Current_Price_Oracle" && mode === "Editing" ? (
        <SaveButton
          ml={"auto"}
          mb={3}
          mr={4}
          fontSize="xs"
          altText={t("Update")}
          onClick={updateOracle}
        />
      ) : null}
    </>
  );
}
Example #23
Source File: AssetSettings.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AssetSettings = ({
  mode,
  poolID,
  poolName,
  tokenData,
  closeModal,
  oracleData,
  oracleModel,
  tokenAddress,
  cTokenAddress,
  existingAssets,
  poolOracleAddress,
  comptrollerAddress,
}: {
  comptrollerAddress: string; // Fuse pool's comptroller address
  poolOracleAddress: string; // Fuse pool's oracle address
  tokenAddress: string; // Underlying token's addres. i.e. USDC, DAI, etc.
  oracleModel: string | undefined; // Fuse pool's oracle model name. i.e MasterPrice, Chainlink, etc.
  oracleData: OracleDataType | undefined; // Fuse pool's oracle contract, admin, overwriting permissions.
  tokenData: TokenData; // Token's data i.e. symbol, logo, css color, etc.
  poolName: string; // Fuse pool's name.
  poolID: string; // Fuse pool's ID.

  // Only for editing mode
  cTokenAddress?: string; // CToken for Underlying token. i.e f-USDC-4

  // Only for add asset modal
  existingAssets?: USDPricedFuseAsset[]; // A list of assets in the pool

  // Modal config
  closeModal: () => any;
  mode: "Editing" | "Adding";
}) => {
  const toast = useToast();
  const { fuse, address } = useRari();
  const queryClient = useQueryClient();
  const isMobile = useIsMediumScreen();

  // Component state
  const [isDeploying, setIsDeploying] = useState(false);

  // Asset's general configurations.
  const [adminFee, setAdminFee] = useState(0);
  const [reserveFactor, setReserveFactor] = useState(10);
  const [isBorrowPaused, setIsBorrowPaused] = useState(false);
  const [collateralFactor, setCollateralFactor] = useState(50);

  const [interestRateModel, setInterestRateModel] = useState(
    Fuse.PUBLIC_INTEREST_RATE_MODEL_CONTRACT_ADDRESSES
      .JumpRateModel_Cream_Stables_Majors
  );

  const curves = useIRMCurves({ interestRateModel, adminFee, reserveFactor });

  // Asset's Oracle Configuration
  const [oracleTouched, setOracleTouched] = useState(false);
  const [activeOracleModel, setActiveOracleModel] = useState<string>(""); // Will store the oracle's model selected for this asset. i.e. Rari Master Price Oracle, Custome Oracle, etc.
  const [oracleAddress, setOracleAddress] = useState<string>(""); // Will store the actual address of the oracle.

  // Uniswap V3 base token oracle config - these following lines are used only
  // if you choose Uniswap V3 Twap Oracle as the asset's oracle.
  const [feeTier, setFeeTier] = useState<number>(0);
  const [uniV3BaseTokenAddress, setUniV3BaseTokenAddress] =
    useState<string>(""); // This will store the pair's base token.
  const [uniV3BaseTokenOracle, setUniV3BaseTokenOracle] = useState<string>(""); // This will store the oracle chosen for the uniV3BaseTokenAddress.
  const [baseTokenActiveOracleName, setBaseTokenActiveOracleName] =
    useState<string>("");
  const [uniV3BaseTokenHasOracle, setUniV3BaseTokenHasOracle] =
    useState<boolean>(false); // Will let us know if fuse pool's oracle has a price feed for the pair's base token.

  // This will be used to index whitelistPools array (fetched from the graph.)
  // It also helps us know if user has selected anything or not. If they have, detail fields are shown.
  const [activeUniSwapPair, setActiveUniSwapPair] = useState<string>("");

  // If uniV3BaseTokenAddress doesn't have an oracle in the fuse pool's oracle, then show the form
  // Or if the baseToken is weth then dont show form because we already have a hardcoded oracle for it
  const shouldShowUniV3BaseTokenOracleForm = useMemo(
    () =>
      !!uniV3BaseTokenAddress &&
      !uniV3BaseTokenHasOracle &&
      !isTokenETHOrWETH(uniV3BaseTokenAddress) &&
      (activeOracleModel === "Uniswap_V3_Oracle" ||
        activeOracleModel === "Uniswap_V2_Oracle" ||
        activeOracleModel === "SushiSwap_Oracle"),
    [uniV3BaseTokenHasOracle, uniV3BaseTokenAddress, activeOracleModel]
  );

  // If you choose a UniV3 Pool as the oracle, check if fuse pool's oracle can get a price for uniV3BaseTokenAddress
  useEffect(() => {
    if (
      !!uniV3BaseTokenAddress &&
      !isTokenETHOrWETH(uniV3BaseTokenAddress) &&
      !!oracleData &&
      typeof oracleData !== "string"
    ) {
      oracleData.oracleContract.methods
        .price(uniV3BaseTokenAddress)
        .call()
        .then((price: string) => {
          // if you're able to get a price for this asset then
          return parseFloat(price) > 0
            ? setUniV3BaseTokenHasOracle(true)
            : setUniV3BaseTokenHasOracle(false);
        })
        .catch((err: any) => {
          console.log("Could not fetch price using pool's oracle");
          setUniV3BaseTokenHasOracle(false);
        });
    }
  }, [uniV3BaseTokenAddress, oracleData, setUniV3BaseTokenHasOracle]);

  // Sharad: New stuff  - to skip oracle step if possible
  const [defaultOracle, setDefaultOracle] = useState<string>(
    ETH_TOKEN_DATA.address
  );
  const [customOracleForToken, setCustomOracleForToken] = useState<string>(
    ETH_TOKEN_DATA.address
  );
  const [priceForAsset, setPriceForAsset] = useState<undefined | number>();

  const hasDefaultOracle = useMemo(
    () => defaultOracle !== ETH_TOKEN_DATA.address,
    [defaultOracle]
  );

  const hasCustomOracleForToken = useMemo(
    () => customOracleForToken !== ETH_TOKEN_DATA.address,
    [customOracleForToken]
  );

  const hasPriceForAsset = useMemo(
    () => !!priceForAsset && priceForAsset > 0,
    [priceForAsset]
  );

  // For this asset, check for a defaultOracle, customOracle, and Pool MPO price for this token
  useEffect(() => {
    // If its a legacy oracle (type === string) then we cant create a MasterPriceOracle isntance for it and the user wont even be able to configure the oracle.
    if (!!oracleData && typeof oracleData !== "string") {
      const mpo = createOracle(poolOracleAddress, fuse, "MasterPriceOracle");

      // 1. Check if it has a default oracle
      mpo.methods
        .defaultOracle()
        .call()
        .then((defaultOracle: string) => {
          // const defaultOracle = createOracle(defaultOracle, fuse, "MasterPriceOracle");
          setDefaultOracle(defaultOracle);
          return mpo.methods.oracles(tokenAddress).call();
        })
        .then((oracleForToken: string) => {
          // 2.) Check for Custom oracle
          setCustomOracleForToken(oracleForToken);
          return mpo.methods.price(tokenAddress).call();
        })
        .then((priceForAsset: string) => {
          // 3.) Check for price
          console.log({ priceForAsset });
          setPriceForAsset(parseFloat(priceForAsset));
        })
        .catch((err: any) => {
          console.error(err);
        });
    }
  }, [oracleData, fuse, tokenAddress, poolOracleAddress]);

  // Modal Pages
  const [stage, setStage] = useState<number>(1);

  const handleSetStage = (incr: number) => {
    const newStage = stage + incr;

    // increment stage
    if (incr > 0) {
      if (isTokenETHOrWETH(tokenAddress) && newStage === 2) {
        setStage(3);
      } else setStage(newStage);
    }

    // decrement (previous page)
    else if (incr < 0) {
      if (isTokenETHOrWETH(tokenAddress) && newStage === 2) {
        setStage(1);
      } else setStage(newStage);
    }
  };

  // Transaction Stepper
  const [activeStep, setActiveStep] = useState<number>(0);

  // Retry Flag - start deploy function from here
  const [retryFlag, setRetryFlag] = useState<RETRY_FLAG>(1);
  const [needsRetry, setNeedsRetry] = useState<boolean>(false);

  // Set transaction steps based on type of Oracle deployed
  const steps: string[] =
    activeOracleModel === "Rari_Default_Oracle" ||
    activeOracleModel === "Chainlink_Oracle"
      ? SimpleDeployment
      : activeOracleModel === "Uniswap_V3_Oracle"
      ? UniSwapV3DeploymentSimple
      : SimpleDeployment;

  const increaseActiveStep = (step: string) => {
    setActiveStep(steps.indexOf(step));
  };

  const preDeployValidate = (oracleAddressToUse: string) => {
    // If pool already contains this asset:
    if (
      existingAssets!.some(
        (asset) => asset.underlyingToken === tokenData.address
      )
    ) {
      toast({
        title: "Error!",
        description: "You have already added this asset to this pool.",
        status: "error",
        duration: 2000,
        isClosable: true,
        position: "top-right",
      });

      throw new Error("You have already added this asset to this pool.");
    }

    // If you have not chosen an oracle
    if (!isTokenETHOrWETH(tokenAddress)) {
      if (oracleAddressToUse === "") {
        toast({
          title: "Error!",
          description: "Please choose a valid oracle for this asset",
          status: "error",
          duration: 2000,
          isClosable: true,
          position: "top-right",
        });

        throw new Error("Please choose a valid oracle for this asset");
      }
    }
  };

  const checkUniV3Oracle = async () => {
    // If this oracle is set in the optional form (only if u have a univ3pair and the base token isnt in the oracle)
    // Then u have to deploy the base token )

    // Check for observation cardinality and fix if necessary
    const shouldPrime = await fuse.checkCardinality(oracleAddress);

    if (shouldPrime) {
      increaseActiveStep("Increasing Uniswap V3 pair cardinality");

      await fuse.primeUniswapV3Oracle(oracleAddress, { from: address });
    }
  };

  const deployUniV3Oracle = async () => {
    increaseActiveStep("Deploying Uniswap V3 Twap Oracle");

    // alert("deploying univ3twapOracle");

    console.log("deployUniV3Oracle", {
      feeTier,
      uniV3BaseTokenAddress,
      address,
      deployPriceOracle: fuse.deployPriceOracle,
    });

    // Deploy UniV3 oracle
    const oracleAddressToUse = await fuse.deployPriceOracle(
      "UniswapV3TwapPriceOracleV2",
      { feeTier, baseToken: uniV3BaseTokenAddress },
      { from: address }
    );

    // alert("finished univ3twapOracle " + oracleAddressToUse);

    console.log({ oracleAddressToUse });

    return oracleAddressToUse;
  };

  // Deploy Oracle
  const deployUniV2Oracle = async () =>
    await fuse.deployPriceOracle(
      "UniswapTwapPriceOracleV2",
      { baseToken: uniV3BaseTokenAddress },
      { from: address }
    );

  const addOraclesToMasterPriceOracle = async (oracleAddressToUse: string) => {
    /** Configure the pool's MasterPriceOracle  **/
    increaseActiveStep("Configuring your Fuse pool's Master Price Oracle");

    // Instantiate Fuse Pool's Oracle contract (Always "MasterPriceOracle")
    const poolOracleContract = createOracle(
      poolOracleAddress,
      fuse,
      "MasterPriceOracle"
    );

    const tokenArray = shouldShowUniV3BaseTokenOracleForm
      ? [tokenAddress, uniV3BaseTokenAddress] // univ3 only
      : [tokenAddress];

    const oracleAddress = shouldShowUniV3BaseTokenOracleForm
      ? [oracleAddressToUse, uniV3BaseTokenOracle] // univ3 only
      : [oracleAddressToUse];

    const hasOracles = await Promise.all(
      tokenArray.map(async (tokenAddr) => {
        const address: string = await poolOracleContract.methods
          .oracles(tokenAddr)
          .call();

        // if address  is EmptyAddress then there is no oracle for this token
        return !(address === "0x0000000000000000000000000000000000000000");
      })
    );

    const tokenHasOraclesInPool = hasOracles.some((x) => !!x);

    console.log({
      hasOracles,
      tokenArray,
      oracleAddress,
      tokenHasOraclesInPool,
    });

    if (!!tokenHasOraclesInPool) return;

    const tx = await poolOracleContract.methods
      .add(tokenArray, oracleAddress)
      .send({ from: address });

    toast({
      title: "You have successfully configured the oracle for this asset!",
      description:
        "Oracle will now point to the new selected address. Now, lets add you asset to the pool.",
      status: "success",
      duration: 2000,
      isClosable: true,
      position: "top-right",
    });
  };

  const deployAssetToPool = async () => {
    increaseActiveStep(
      "Configuring your Fuse pool to support new asset market"
    );

    // 50% -> 0.5 * 1e18
    const bigCollateralFactor = new BigNumber(collateralFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    // 10% -> 0.1 * 1e18
    const bigReserveFactor = new BigNumber(reserveFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    // 5% -> 0.05 * 1e18
    const bigAdminFee = new BigNumber(adminFee)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    const conf: any = {
      underlying: tokenData.address,
      comptroller: comptrollerAddress,
      interestRateModel,
      initialExchangeRateMantissa: fuse.web3.utils.toBN(1e18),

      // Ex: BOGGED USDC
      name: poolName + " " + tokenData.name,
      // Ex: fUSDC-456
      symbol: "f" + tokenData.symbol + "-" + poolID,
      decimals: 8,
    };

    console.log({
      conf,
      bigCollateralFactor,
      bigReserveFactor,
      bigAdminFee,
      address,
    });

    await fuse.deployAsset(
      conf,
      bigCollateralFactor,
      bigReserveFactor,
      bigAdminFee,
      { from: address },
      // TODO: Disable this. This bypasses the price feed check. Only using now because only trusted partners are deploying assets.
      true
    );

    increaseActiveStep("All Done!");
  };

  // Deploy Asset!
  const deploy = async () => {
    let oracleAddressToUse = oracleAddress;
    try {
      preDeployValidate(oracleAddressToUse);
    } catch (err) {
      return;
    }

    setIsDeploying(true);

    let _retryFlag = retryFlag;

    try {
      // It should be 1 if we haven't had to retry anything

      /** IF UNISWAP V3 ORACLE **/
      if (_retryFlag === 1) {
        setNeedsRetry(false);
        if (activeOracleModel === "Uniswap_V3_Oracle") {
          console.log("preCheck");
          await checkUniV3Oracle();
          console.log("postCheck");
        }
        _retryFlag = 2; // set it to two after we fall through step 1
      }

      /** IF UNISWAP V3 ORACLE **/
      if (_retryFlag === 2) {
        setNeedsRetry(false);
        if (activeOracleModel === "Uniswap_V3_Oracle") {
          console.log("predeploy");
          oracleAddressToUse = await deployUniV3Oracle();
          console.log("postDeploy", { oracleAddressToUse });
        }
        _retryFlag = 3;
      }

      /** IF UNISWAP V2 ORACLE **/
      if (_retryFlag === 3) {
        setNeedsRetry(false);
        if (activeOracleModel === "Uniswap_V2_Oracle") {
          oracleAddressToUse = await deployUniV2Oracle();
        }
        _retryFlag = 4;
      }

      /**  CONFIGURE MASTERPRICEORACLE **/
      // You dont need to configure if your asset is ETH / WETH
      // You dont need to configure if a default oracle is available and you have chosen it

      if (_retryFlag === 4) {
        setNeedsRetry(false);
        if (
          !isTokenETHOrWETH(tokenAddress) &&
          (oracleModel === "MasterPriceOracleV3" ||
            oracleModel === "MasterPriceOracleV2") &&
          oracleAddress !== defaultOracle // If you have not selected the default oracle you will have to configure.
        ) {
          // alert("addOraclesToMasterPriceOracle");
          await addOraclesToMasterPriceOracle(oracleAddressToUse);
        }
        _retryFlag = 5;
      }

      /** DEPLOY ASSET  **/
      if (_retryFlag === 5) {
        setNeedsRetry(false);

        await deployAssetToPool();

        LogRocket.track("Fuse-DeployAsset");

        queryClient.refetchQueries();
        // Wait 2 seconds for refetch and then close modal.
        // We do this instead of waiting the refetch because some refetches take a while or error out and we want to close now.
        await new Promise((resolve) => setTimeout(resolve, 2000));

        toast({
          title: "You have successfully added an asset to this pool!",
          description: "You may now lend and borrow with this asset.",
          status: "success",
          duration: 2000,
          isClosable: true,
          position: "top-right",
        });
      }

      closeModal();
    } catch (e) {
      handleGenericError(e, toast);
      setRetryFlag(_retryFlag);
      console.log({ _retryFlag });
      setNeedsRetry(true);
    }
  };

  // Update values on refetch!
  const cTokenData = useCTokenData(comptrollerAddress, cTokenAddress);

  useEffect(() => {
    if (cTokenData) {
      setIsBorrowPaused(cTokenData.isPaused);
      setAdminFee(cTokenData.adminFeeMantissa / 1e16);
      setReserveFactor(cTokenData.reserveFactorMantissa / 1e16);
      setInterestRateModel(cTokenData.interestRateModelAddress);
      setCollateralFactor(cTokenData.collateralFactorMantissa / 1e16);
    }
  }, [cTokenData]);

  const args2: AddAssetContextData = {
    mode,
    isDeploying,
    setIsDeploying,
    adminFee,
    setAdminFee,
    reserveFactor,
    setReserveFactor,
    isBorrowPaused,
    setIsBorrowPaused,
    collateralFactor,
    setCollateralFactor,
    interestRateModel,
    setInterestRateModel,
    curves,
    oracleTouched,
    setOracleTouched,
    activeOracleModel,
    setActiveOracleModel,
    oracleAddress,
    setOracleAddress,
    feeTier,
    setFeeTier,
    uniV3BaseTokenAddress,
    setUniV3BaseTokenAddress: setUniV3BaseTokenAddress,
    uniV3BaseTokenOracle,
    setUniV3BaseTokenOracle,
    baseTokenActiveOracleName,
    setBaseTokenActiveOracleName,
    uniV3BaseTokenHasOracle,
    setUniV3BaseTokenHasOracle,
    activeUniSwapPair,
    setActiveUniSwapPair,
    shouldShowUniV3BaseTokenOracleForm,
    defaultOracle,
    setDefaultOracle,
    customOracleForToken,
    setCustomOracleForToken,
    priceForAsset,
    setPriceForAsset,
    hasDefaultOracle,
    hasCustomOracleForToken,
    hasPriceForAsset,
    stage,
    setStage,
    handleSetStage,
    activeStep,
    setActiveStep,
    increaseActiveStep,
    retryFlag,
    setRetryFlag,
    needsRetry,
    setNeedsRetry,
    cTokenData,
    cTokenAddress,
    oracleData,
    poolOracleAddress,
    poolOracleModel: oracleModel,
    tokenData,
    tokenAddress,
    comptrollerAddress,
  };

  if (mode === "Editing")
    return (
      <AddAssetContext.Provider value={args2}>
        <AssetConfig />
      </AddAssetContext.Provider>
    );

  return (
    cTokenAddress ? cTokenData?.cTokenAddress === cTokenAddress : true
  ) ? (
    <AddAssetContext.Provider value={args2}>
      <Column
        mainAxisAlignment="center"
        crossAxisAlignment="center"
        height="100%"
        minHeight="100%"
      >
        <Row
          mainAxisAlignment={"center"}
          crossAxisAlignment={"center"}
          w="100%"
          flexBasis={"10%"}
          // bg="green"
        >
          <Title stage={stage} />
        </Row>
        <RowOrColumn
          maxHeight="70%"
          isRow={!isMobile}
          crossAxisAlignment={stage < 3 ? "flex-start" : "center"}
          mainAxisAlignment={stage < 3 ? "flex-start" : "center"}
          height={!isDeploying ? "70%" : "60%"}
          width="100%"
          overflowY="auto"
          flexBasis={"80%"}
          flexGrow={1}
          // bg="red"
        >
          {stage === 1 ? (
            <Column
              width="100%"
              height="100%"
              d="flex"
              direction="row"
              mainAxisAlignment="center"
              crossAxisAlignment="center"
              alignItems="center"
              justifyContent="center"
            >
              <Screen1 />
            </Column>
          ) : stage === 2 ? (
            <Row
              width="100%"
              height="100%"
              d="flex"
              mainAxisAlignment="center"
              crossAxisAlignment="center"
              alignItems="center"
              justifyContent="center"
              // bg="aqua"
            >
              <Screen2 mode="Adding" />
            </Row>
          ) : (
            <Screen3 />
            // <Heading>SCREEN3</Heading>
          )}
        </RowOrColumn>
        <DeployButton steps={steps} deploy={deploy} />
        {/* {needsRetry && <Button onClick={deploy}>Retry</Button>} */}
      </Column>
    </AddAssetContext.Provider>
  ) : (
    <Center expand>
      <Spinner my={8} />
    </Center>
  );
}
Example #24
Source File: AssetConfig.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AssetConfig = () => {
  const queryClient = useQueryClient();
  const { fuse, address } = useRari();
  const { t } = useTranslation();
  const toast = useToast();

  const {
    cTokenData,
    collateralFactor,
    setCollateralFactor,
    cTokenAddress,
    isBorrowPaused,
    adminFee,
    setAdminFee,
    activeOracleModel,
    oracleData,
    tokenAddress,
    mode,
    setInterestRateModel,
    interestRateModel,
    tokenData,
    setReserveFactor,
    reserveFactor,
    comptrollerAddress,
  } = useAddAssetContext();

  const curves = useIRMCurves({ interestRateModel, adminFee, reserveFactor });

  // Liquidation incentive. (This is configured at pool level)
  const liquidationIncentiveMantissa =
    useLiquidationIncentive(comptrollerAddress);

  const scaleCollateralFactor = (_collateralFactor: number) => {
    return _collateralFactor / 1e16;
  };

  const scaleReserveFactor = (_reserveFactor: number) => {
    return _reserveFactor / 1e16;
  };

  const scaleAdminFee = (_adminFee: number) => {
    return _adminFee / 1e16;
  };

  // Updates asset's Interest Rate Model.
  const updateInterestRateModel = async () => {
    const cToken = createCToken(fuse, cTokenAddress!);

    try {
      await testForCTokenErrorAndSend(
        cToken.methods._setInterestRateModel(interestRateModel),
        address,
        ""
      );

      LogRocket.track("Fuse-UpdateInterestRateModel");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  // Determines if users can borrow an asset or not.
  const togglePause = async () => {
    const comptroller = createComptroller(comptrollerAddress, fuse);

    try {
      await comptroller.methods
        ._setBorrowPaused(cTokenAddress, !isBorrowPaused)
        .send({ from: address });

      LogRocket.track("Fuse-PauseToggle");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  // Updates loan to Value ratio.
  const updateCollateralFactor = async () => {
    const comptroller = createComptroller(comptrollerAddress, fuse);

    // 70% -> 0.7 * 1e18
    const bigCollateralFactor = new BigNumber(collateralFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    try {
      await testForComptrollerErrorAndSend(
        comptroller.methods._setCollateralFactor(
          cTokenAddress,
          bigCollateralFactor
        ),
        address,
        ""
      );

      LogRocket.track("Fuse-UpdateCollateralFactor");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  // Updated portion of accrued reserves that goes into reserves.
  const updateReserveFactor = async () => {
    const cToken = createCToken(fuse, cTokenAddress!);

    // 10% -> 0.1 * 1e18
    const bigReserveFactor = new BigNumber(reserveFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);
    try {
      await testForCTokenErrorAndSend(
        cToken.methods._setReserveFactor(bigReserveFactor),
        address,
        ""
      );

      LogRocket.track("Fuse-UpdateReserveFactor");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  // Updates asset's admin fee.
  const updateAdminFee = async () => {
    const cToken = createCToken(fuse, cTokenAddress!);

    // 5% -> 0.05 * 1e18
    const bigAdminFee = new BigNumber(adminFee)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    try {
      await testForCTokenErrorAndSend(
        cToken.methods._setAdminFee(bigAdminFee),
        address,
        ""
      );

      LogRocket.track("Fuse-UpdateAdminFee");

      queryClient.refetchQueries();
    } catch (e) {
      handleGenericError(e, toast);
    }
  };

  return (
    <>
      <Column
        width={mode === "Editing" ? "100%" : "60%"}
        maxWidth={mode === "Editing" ? "100%" : "50%"}
        height="100%"
        overflowY="auto"
        mainAxisAlignment={mode === "Adding" ? "center" : "flex-start"}
        crossAxisAlignment={mode === "Adding" ? "center" : "flex-start"}
      >
        {mode === "Editing" ? (
          <>
            <MarketCapConfigurator
              mode="Borrow"
              tokenData={tokenData}
              cTokenAddress={cTokenAddress}
              comptrollerAddress={comptrollerAddress}
            />

            <ModalDivider />

            <MarketCapConfigurator
              mode="Supply"
              tokenData={tokenData}
              cTokenAddress={cTokenAddress}
              comptrollerAddress={comptrollerAddress}
            />
          </>
        ) : null}

        <ModalDivider />

        <ConfigRow height="35px">
          <SimpleTooltip
            label={t(
              "Collateral factor can range from 0-90%, and represents the proportionate increase in liquidity (borrow limit) that an account receives by depositing the asset."
            )}
          >
            <Text fontWeight="bold">
              {t("Collateral Factor")} <QuestionIcon ml={1} mb="4px" />
            </Text>
          </SimpleTooltip>

          {cTokenData !== undefined &&
          mode === "Editing" &&
          collateralFactor !==
            scaleCollateralFactor(cTokenData?.collateralFactorMantissa) ? (
            <SaveButton ml={3} onClick={updateCollateralFactor} />
          ) : null}

          <SliderWithLabel
            ml="auto"
            value={collateralFactor}
            setValue={setCollateralFactor}
            formatValue={formatPercentage}
            step={0.5}
            max={
              liquidationIncentiveMantissa
                ? // 100% CF - Liquidation Incentive (ie: 8%) - 5% buffer
                  100 -
                  (liquidationIncentiveMantissa.toString() / 1e16 - 100) -
                  5
                : 90
            }
          />
        </ConfigRow>

        <ModalDivider />

        {cTokenAddress ? (
          <ConfigRow>
            <SimpleTooltip
              label={t("If enabled borrowing this asset will be disabled.")}
            >
              <Text fontWeight="bold">
                {t("Pause Borrowing")} <QuestionIcon ml={1} mb="4px" />
              </Text>
            </SimpleTooltip>

            <SaveButton
              ml="auto"
              onClick={togglePause}
              fontSize="xs"
              altText={
                isBorrowPaused ? t("Enable Borrowing") : t("Pause Borrowing")
              }
            />
          </ConfigRow>
        ) : null}

        <ModalDivider />

        <ConfigRow height="35px">
          <SimpleTooltip
            label={t(
              "The fraction of interest generated on a given asset that is routed to the asset's Reserve Pool. The Reserve Pool protects lenders against borrower default and liquidation malfunction."
            )}
          >
            <Text fontWeight="bold">
              {t("Reserve Factor")} <QuestionIcon ml={1} mb="4px" />
            </Text>
          </SimpleTooltip>

          {cTokenData &&
          reserveFactor !==
            scaleReserveFactor(cTokenData.reserveFactorMantissa) ? (
            <SaveButton ml={3} onClick={updateReserveFactor} />
          ) : null}

          <SliderWithLabel
            ml="auto"
            value={reserveFactor}
            setValue={setReserveFactor}
            formatValue={formatPercentage}
            max={50}
          />
        </ConfigRow>
        <ModalDivider />

        <ConfigRow height="35px">
          <SimpleTooltip
            label={t(
              "The fraction of interest generated on a given asset that is routed to the asset's admin address as a fee."
            )}
          >
            <Text fontWeight="bold">
              {t("Admin Fee")} <QuestionIcon ml={1} mb="4px" />
            </Text>
          </SimpleTooltip>

          {cTokenData &&
          adminFee !== scaleAdminFee(cTokenData.adminFeeMantissa) ? (
            <SaveButton ml={3} onClick={updateAdminFee} />
          ) : null}

          <SliderWithLabel
            ml="auto"
            value={adminFee}
            setValue={setAdminFee}
            formatValue={formatPercentage}
            max={30}
          />
        </ConfigRow>

        <ModalDivider />

        {(activeOracleModel === "MasterPriceOracleV2" ||
          activeOracleModel === "MasterPriceOracleV3") &&
          oracleData !== undefined &&
          !isTokenETHOrWETH(tokenAddress) &&
          mode === "Editing" && (
            <>
              <OracleConfig />

              <ModalDivider />
            </>
          )}

        <ModalDivider />

        <ConfigRow>
          <SimpleTooltip
            label={t(
              "The interest rate model chosen for an asset defines the rates of interest for borrowers and suppliers at different utilization levels."
            )}
          >
            <Text fontWeight="bold">
              {t("Interest Model")} <QuestionIcon ml={1} mb="4px" />
            </Text>
          </SimpleTooltip>

          <Select
            {...DASHBOARD_BOX_PROPS}
            ml="auto"
            borderRadius="7px"
            fontWeight="bold"
            _focus={{ outline: "none" }}
            width="260px"
            value={interestRateModel.toLowerCase()}
            onChange={(event) => setInterestRateModel(event.target.value)}
          >
            {Object.entries(
              Fuse.PUBLIC_INTEREST_RATE_MODEL_CONTRACT_ADDRESSES
            ).map(([key, value]) => {
              return (
                <option
                  className="black-bg-option"
                  value={value.toLowerCase()}
                  key={key}
                >
                  {key}
                </option>
              );
            })}
          </Select>

          {cTokenData &&
          cTokenData.interestRateModelAddress.toLowerCase() !==
            interestRateModel.toLowerCase() ? (
            <SaveButton
              height="40px"
              borderRadius="7px"
              onClick={updateInterestRateModel}
            />
          ) : null}
        </ConfigRow>

        {mode === "Editing" && (
          <IRMChart curves={curves} tokenData={tokenData} />
        )}
      </Column>

      {mode === "Adding" ? (
        <Column
          width="40%"
          height="100%"
          overflowY="auto"
          mainAxisAlignment={mode === "Adding" ? "center" : "flex-start"}
          crossAxisAlignment={mode === "Adding" ? "center" : "flex-start"}
        >
          <IRMChart curves={curves} tokenData={tokenData} />
          <Text textAlign="center">
            {fuse
              .identifyInterestRateModelName(interestRateModel)
              .replace("_", " ")}
          </Text>
        </Column>
      ) : null}
    </>
  );
}
Example #25
Source File: FusePoolPage.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
AssetSupplyRow = ({
  assets,
  index,
  comptrollerAddress,
  supplyIncentives,
  rewardTokensData,
  isPaused,
}: {
  assets: USDPricedFuseAsset[];
  index: number;
  comptrollerAddress: string;
  supplyIncentives: CTokenRewardsDistributorIncentivesWithRates[];
  rewardTokensData: TokensDataMap;
  isPaused: boolean;
}) => {
  const {
    isOpen: isModalOpen,
    onOpen: openModal,
    onClose: closeModal,
  } = useDisclosure();

  const authedOpenModal = useAuthedCallback(openModal);

  const asset = assets[index];

  const { fuse, address } = useRari();

  const tokenData = useTokenData(asset.underlyingToken);

  const supplyAPY = convertMantissaToAPY(asset.supplyRatePerBlock, 365);

  const queryClient = useQueryClient();

  const toast = useToast();

  const onToggleCollateral = async () => {
    const comptroller = createComptroller(comptrollerAddress, fuse);

    let call;
    if (asset.membership) {
      call = comptroller.methods.exitMarket(asset.cToken);
    } else {
      call = comptroller.methods.enterMarkets([asset.cToken]);
    }

    let response = await call.call({ from: address });
    // For some reason `response` will be `["0"]` if no error but otherwise it will return a string number.
    if (response[0] !== "0") {
      if (asset.membership) {
        toast({
          title: "Error! Code: " + response,
          description:
            "You cannot disable this asset as collateral as you would not have enough collateral posted to keep your borrow. Try adding more collateral of another type or paying back some of your debt.",
          status: "error",
          duration: 9000,
          isClosable: true,
          position: "top-right",
        });
      } else {
        toast({
          title: "Error! Code: " + response,
          description:
            "You cannot enable this asset as collateral at this time.",
          status: "error",
          duration: 9000,
          isClosable: true,
          position: "top-right",
        });
      }

      return;
    }

    await call.send({ from: address });

    LogRocket.track("Fuse-ToggleCollateral");

    queryClient.refetchQueries();
  };

  const isStakedOHM =
    asset.underlyingToken.toLowerCase() ===
    "0x04F2694C8fcee23e8Fd0dfEA1d4f5Bb8c352111F".toLowerCase();

  const { data: stakedOHMApyData } = useQuery("sOHM_APY", async () => {
    const data = (
      await fetch("https://api.rari.capital/fuse/pools/18/apy")
    ).json();

    return data as Promise<{ supplyApy: number; supplyWpy: number }>;
  });

  const isMobile = useIsMobile();

  const { t } = useTranslation();

  const hasSupplyIncentives = !!supplyIncentives.length;
  const totalSupplyAPR =
    supplyIncentives?.reduce((prev, incentive) => {
      const apr = incentive.supplyAPR;
      return prev + apr;
    }, 0) ?? 0;

  const [hovered, setHovered] = useState<number>(-1);

  const handleMouseEnter = (index: number) => setHovered(index);
  const handleMouseLeave = () => setHovered(-1);

  const displayedSupplyAPR =
    hovered >= 0 ? supplyIncentives[hovered].supplyAPR : totalSupplyAPR;

  const displayedSupplyAPRLabel =
    hovered >= 0
      ? `${supplyIncentives[hovered].supplyAPR.toFixed(2)} % APR in ${
          rewardTokensData[supplyIncentives[hovered].rewardToken].symbol
        } distributions.`
      : `${displayedSupplyAPR.toFixed(
          2
        )}% total APR distributed in ${supplyIncentives
          .map((incentive) => rewardTokensData[incentive.rewardToken].symbol)
          .join(", ")}
         `;

  const _hovered = hovered > 0 ? hovered : 0;

  const color =
    rewardTokensData[supplyIncentives?.[_hovered]?.rewardToken]?.color ??
    "white";

  const symbol = getSymbol(tokenData, asset);

  return (
    <>
      <PoolModal
        defaultMode={Mode.SUPPLY}
        comptrollerAddress={comptrollerAddress}
        assets={assets}
        index={index}
        isOpen={isModalOpen}
        onClose={closeModal}
        isBorrowPaused={asset.isPaused}
      />

      <Row
        mainAxisAlignment="flex-start"
        crossAxisAlignment="flex-start"
        width="100%"
        px={4}
        py={1.5}
        className="hover-row"
      >
        {/* Underlying Token Data */}
        <Column
          mainAxisAlignment="flex-start"
          crossAxisAlignment="flex-start"
          width="27%"
        >
          <Row
            mainAxisAlignment="flex-start"
            crossAxisAlignment="center"
            width="100%"
            as="button"
            onClick={authedOpenModal}
          >
            <Avatar
              bg="#FFF"
              boxSize="37px"
              name={symbol}
              src={
                tokenData?.logoURL ??
                "https://raw.githubusercontent.com/feathericons/feather/master/icons/help-circle.svg"
              }
            />
            <Text fontWeight="bold" fontSize="lg" ml={2} flexShrink={0}>
              {symbol}
            </Text>
          </Row>
          {/* <Row
            mainAxisAlignment="flex-start"
            crossAxisAlignment="center"
            width="100%"
          >
            <Text fontSize="sm" ml={2} flexShrink={0}>
            {shortUsdFormatter(asset.liquidityUSD)}
            </Text>
          </Row> */}
        </Column>

        {/* APY */}
        {isMobile ? null : (
          <Column
            mainAxisAlignment="flex-start"
            crossAxisAlignment="flex-end"
            width="27%"
            as="button"
            onClick={authedOpenModal}
          >
            <Text
              color={tokenData?.color ?? "#FF"}
              fontWeight="bold"
              fontSize="17px"
            >
              {isStakedOHM
                ? stakedOHMApyData
                  ? (stakedOHMApyData.supplyApy * 100).toFixed(2)
                  : "?"
                : supplyAPY.toFixed(2)}
              %
            </Text>

            {/* Demo Supply Incentives */}
            {hasSupplyIncentives && (
              <Row
                // ml={1}
                // mb={.5}
                crossAxisAlignment="center"
                mainAxisAlignment="flex-end"
                py={2}
              >
                <Text fontWeight="bold" mr={1}>
                  +
                </Text>
                <AvatarGroup size="xs" max={30} ml={2} mr={1} spacing={1}>
                  {supplyIncentives?.map((supplyIncentive, i) => {
                    return (
                      <SimpleTooltip label={displayedSupplyAPRLabel}>
                        <CTokenIcon
                          address={supplyIncentive.rewardToken}
                          boxSize="20px"
                          onMouseEnter={() => handleMouseEnter(i)}
                          onMouseLeave={() => handleMouseLeave()}
                          _hover={{
                            zIndex: 9,
                            border: ".5px solid white",
                            transform: "scale(1.3);",
                          }}
                        />
                      </SimpleTooltip>
                    );
                  })}
                </AvatarGroup>
                <SimpleTooltip label={displayedSupplyAPRLabel}>
                  <Text color={color} fontWeight="bold" pl={1} fontSize="sm">
                    {/* {(supplyIncentive.supplySpeed / 1e18).toString()}%  */}
                    {displayedSupplyAPR.toFixed(2)}% APR
                  </Text>
                </SimpleTooltip>
              </Row>
            )}

            {/* Incentives */}
            {/* {hasSupplyIncentives && (
              <Column
                mainAxisAlignment="flex-start"
                crossAxisAlignment="flex-end"
                py={1}
              >
                {supplyIncentives?.map((supplyIncentive) => {
                  return (
                    <Row
                      ml={1}
                      py={0.5}
                      // mb={.5}
                      crossAxisAlignment="center"
                      mainAxisAlignment="flex-end"
                    >
                      <Text fontWeight="bold" mr={2}>
                        +
                      </Text>
                      <CTokenIcon
                        address={supplyIncentive.rewardToken}
                        boxSize="20px"
                      />
                      <Text fontWeight="bold" mr={2}></Text>
                      <Text
                        color={
                          rewardTokensData[supplyIncentive.rewardToken].color ??
                          "white"
                        }
                        fontWeight="bold"
                      >
                        {(supplyIncentive.supplySpeed / 1e18).toString()}%
                      </Text>
                    </Row>
                  );
                })}
              </Column>
            )} */}

            <SimpleTooltip
              label={t(
                "The Collateral Factor (CF) ratio defines the maximum amount of tokens in the pool that can be borrowed with a specific collateral. It’s expressed in percentage: if in a pool ETH has 75% LTV, for every 1 ETH worth of collateral, borrowers will be able to borrow 0.75 ETH worth of other tokens in the pool."
              )}
            >
              <Text fontSize="sm">{asset.collateralFactor / 1e16}% CF</Text>
            </SimpleTooltip>

            {/* Incentives under APY
            <Column
              mainAxisAlignment="flex-start"
              crossAxisAlignment="flex-end"
              my={1}
            >
              {supplyIncentives?.map((supplyIncentive) => {
                return (
                  <Row
                    mainAxisAlignment="space-between"
                    crossAxisAlignment="center"
                    w="100%"
                  >
                    <Avatar
                      src={
                        rewardTokensData[supplyIncentive.rewardToken].logoURL ?? ""
                      }
                      boxSize="20px"
                    />
                    <Text
                      ml={2}
                      fontWeight="bold"
                      color={
                        rewardTokensData[supplyIncentive.rewardToken].color ?? ""
                      }
                    >
                      {(supplyIncentive.supplySpeed / 1e18).toString()}%
                    </Text>
                  </Row>
                );
              })}
            </Column>
             */}
          </Column>
        )}

        {/* Incentives */}
        {/* <Column mainAxisAlignment="flex-start" crossAxisAlignment="flex-start">
          {supplyIncentives?.map((supplyIncentive) => {
            return (
              <Row mainAxisAlignment="flex-start" crossAxisAlignment="center">
                <Avatar
                  src={rewardTokensData[supplyIncentive.rewardToken].logoURL}
                  boxSize="15px"
                />
                <Box>
                  {(supplyIncentive.supplySpeed / 1e18).toString()}% APY
                </Box>
              </Row>
            );
          })}
        </Column> */}

        <Column
          mainAxisAlignment="flex-start"
          crossAxisAlignment="flex-end"
          width={isMobile ? "40%" : "27%"}
          as="button"
          onClick={authedOpenModal}
        >
          <Text
            color={tokenData?.color ?? "#FFF"}
            fontWeight="bold"
            fontSize="17px"
          >
            {smallUsdFormatter(asset.supplyBalanceUSD)}
          </Text>

          <Text fontSize="sm">
            {smallUsdFormatter(
              asset.supplyBalance / 10 ** asset.underlyingDecimals
            ).replace("$", "")}{" "}
            {symbol}
          </Text>
        </Column>

        {/* Set As Collateral  */}
        <Row
          width={isMobile ? "34%" : "20%"}
          mainAxisAlignment="flex-end"
          crossAxisAlignment="center"
        >
          <SwitchCSS symbol={symbol} color={tokenData?.color} />
          <Switch
            isChecked={asset.membership}
            className={symbol + "-switch"}
            onChange={onToggleCollateral}
            size="md"
            mt={1}
            mr={5}
          />
        </Row>
      </Row>
    </>
  );
}
Example #26
Source File: FusePoolCreatePage.tsx    From rari-dApp with GNU Affero General Public License v3.0 4 votes vote down vote up
PoolConfiguration = () => {
  const { t } = useTranslation();
  const toast = useToast();
  const { fuse, address } = useRari();
  const navigate = useNavigate();

  const { isOpen, onOpen, onClose } = useDisclosure();

  const [name, setName] = useState("");
  const [isWhitelisted, setIsWhitelisted] = useState(false);
  const [whitelist, setWhitelist] = useState<string[]>([]);

  const [closeFactor, setCloseFactor] = useState(50);
  const [liquidationIncentive, setLiquidationIncentive] = useState(8);

  const [isUsingMPO, setIsUsingMPO] = useState(true)
  const [customOracleAddress, setCustomOracleAddress] = useState('')

  const [isCreating, setIsCreating] = useState(false);

  const [activeStep, setActiveStep] = useState<number>(0);

  const increaseActiveStep = (step: string) => {
    setActiveStep(steps.indexOf(step));
  };

  const [needsRetry, setNeedsRetry] = useState<boolean>(false);
  const [retryFlag, setRetryFlag] = useState<number>(1);

  const [deployedPriceOracle, setDeployedPriceOracle] = useState<string>("");

  const postDeploymentHandle = (priceOracle: string) => {
    setDeployedPriceOracle(priceOracle);
  };

  const deployPool = async (
    bigCloseFactor: string,
    bigLiquidationIncentive: string,
    options: any,
    priceOracle: string
  ) => {
    const [poolAddress] = await fuse.deployPool(
      name,
      isWhitelisted,
      bigCloseFactor,
      
      bigLiquidationIncentive,
      priceOracle,
      {},
      options,
      isWhitelisted ? whitelist : null
    );

    return poolAddress;
  };

  const onDeploy = async () => {
    let priceOracle = deployedPriceOracle;

    if (name === "") {
      toast({
        title: "Error!",
        description: "You must specify a name for your Fuse pool!",
        status: "error",
        duration: 2000,
        isClosable: true,
        position: "top-right",
      });

      return;
    }

    if (isWhitelisted && whitelist.length < 2 ) {
      toast({
        title: "Error!",
        description: "You must add an address to your whitelist!",
        status:"error",
        duration: 2000,
        isClosable: true,
        position: "top-right",
      })

      return
    }

    if (!isUsingMPO && !fuse.web3.utils.isAddress(customOracleAddress)) {
      
      toast({
        title: "Error!",
        description: "You must add an address for your oracle or use the default oracle.",
        status:"error",
        duration: 2000,
        isClosable: true,
        position: "top-right",
      })

      return
  }


    setIsCreating(true);
    onOpen();

    // 50% -> 0.5 * 1e18
    const bigCloseFactor = new BigNumber(closeFactor)
      .dividedBy(100)
      .multipliedBy(1e18)
      .toFixed(0);

    // 8% -> 1.08 * 1e8
    const bigLiquidationIncentive = new BigNumber(liquidationIncentive)
      .dividedBy(100)
      .plus(1)
      .multipliedBy(1e18)
      .toFixed(0);

    let _retryFlag = retryFlag;

    try {
      const options = { from: address };

      setNeedsRetry(false);

      if (!isUsingMPO && _retryFlag === 1) {
        _retryFlag = 2;
        priceOracle = customOracleAddress
      }

      if (_retryFlag === 1) {
        priceOracle = await fuse.deployPriceOracle(
          "MasterPriceOracle",
          {
            underlyings: [],
            oracles: [],
            canAdminOverwrite: true,
            defaultOracle:
              Fuse.PUBLIC_PRICE_ORACLE_CONTRACT_ADDRESSES.MasterPriceOracle, // We give the MasterPriceOracle a default "fallback" oracle of the Rari MasterPriceOracle
          },
          options
        );
        postDeploymentHandle(priceOracle);
        increaseActiveStep("Deploying Pool!");
        _retryFlag = 2;
      }

      let poolAddress: string;

      if (_retryFlag === 2) {
        poolAddress = await deployPool(
          bigCloseFactor,
          bigLiquidationIncentive,
          options,
          priceOracle
        );

        const event = (
          await fuse.contracts.FusePoolDirectory.getPastEvents(
            "PoolRegistered",
            {
              fromBlock: (await fuse.web3.eth.getBlockNumber()) - 10,
              toBlock: "latest",
            }
          )
        ).filter(
          (event) =>
            event.returnValues.pool.comptroller.toLowerCase() ===
            poolAddress.toLowerCase()
        )[0];

        LogRocket.track("Fuse-CreatePool");

        toast({
          title: "Your pool has been deployed!",
          description: "You may now add assets to it.",
          status: "success",
          duration: 2000,
          isClosable: true,
          position: "top-right",
        });

        let id = event.returnValues.index;
        onClose();
        navigate(`/fuse/pool/${id}/edit`);
      }
    } catch (e) {
      handleGenericError(e, toast);
      setRetryFlag(_retryFlag);
      setNeedsRetry(true);
    }
  };

  return (
    <>
      <TransactionStepperModal
        handleRetry={onDeploy}
        needsRetry={needsRetry}
        activeStep={activeStep}
        isOpen={isOpen}
        onClose={onClose}
      />
      <DashboardBox width="100%" mt={4}>
        <Column mainAxisAlignment="flex-start" crossAxisAlignment="flex-start">
          <Heading size="sm" px={4} py={4}>
            {t("Create Pool")}
          </Heading>

          <ModalDivider />

          <OptionRow>
            <Text fontWeight="bold" mr={4}>
              {t("Name")}
            </Text>
            <Input
              width="20%"
              value={name}
              onChange={(event) => setName(event.target.value)}
            />
          </OptionRow>

          <ModalDivider />

          <ModalDivider />

          <OptionRow>
            <SimpleTooltip
              label={t(
                "If enabled you will be able to limit the ability to supply to the pool to a select group of addresses. The pool will not show up on the 'all pools' list."
              )}
            >
              <Text fontWeight="bold">
                {t("Whitelisted")} <QuestionIcon ml={1} mb="4px" />
              </Text>
            </SimpleTooltip>

            <Switch
              h="20px"
              isChecked={isWhitelisted}
              onChange={() => {
                setIsWhitelisted((past) => !past);
                // Add the user to the whitelist by default
                if (whitelist.length === 0) {
                  setWhitelist([address]);
                }
              }}
              className="black-switch"
              colorScheme="#121212"
            />
          </OptionRow>

          {isWhitelisted ? (
            <WhitelistInfo
              whitelist={whitelist}
              addToWhitelist={(user) => {
                setWhitelist((past) => [...past, user]);
              }}
              removeFromWhitelist={(user) => {
                setWhitelist((past) =>
                  past.filter(function (item) {
                    return item !== user;
                  })
                );
              }}
            />
          ) : null}

          <ModalDivider />

          <OptionRow>
            <SimpleTooltip
              label={t(
                "The percent, ranging from 0% to 100%, of a liquidatable account's borrow that can be repaid in a single liquidate transaction. If a user has multiple borrowed assets, the closeFactor applies to any single borrowed asset, not the aggregated value of a user’s outstanding borrowing. Compound's close factor is 50%."
              )}
            >
              <Text fontWeight="bold">
                {t("Close Factor")} <QuestionIcon ml={1} mb="4px" />
              </Text>
            </SimpleTooltip>

            <SliderWithLabel
              value={closeFactor}
              setValue={setCloseFactor}
              formatValue={formatPercentage}
              min={5}
              max={90}
            />
          </OptionRow>

          <ModalDivider />

          <OptionRow>
            <SimpleTooltip
              label={t(
                "The additional collateral given to liquidators as an incentive to perform liquidation of underwater accounts. For example, if the liquidation incentive is 10%, liquidators receive an extra 10% of the borrowers collateral for every unit they close. Compound's liquidation incentive is 8%."
              )}
            >
              <Text fontWeight="bold">
                {t("Liquidation Incentive")} <QuestionIcon ml={1} mb="4px" />
              </Text>
            </SimpleTooltip>

            <SliderWithLabel
              value={liquidationIncentive}
              setValue={setLiquidationIncentive}
              formatValue={formatPercentage}
              min={0}
              max={50}
            />
          </OptionRow>

          <ModalDivider />

          <OptionRow>
            <SimpleTooltip
              label={t(
                "We will deploy a price oracle for your pool. This price oracle will contain price feeds for popular ERC20 tokens."
              )}
            >
              <Text fontWeight="bold">
                {isUsingMPO ? t("Default Price Oracle") : t("Custom Price Oracle")} <QuestionIcon ml={1} mb="4px" />
              </Text>
            </SimpleTooltip>

            <Box display="flex" alignItems='flex-end' flexDirection="column"> 
              <Checkbox
                isChecked={isUsingMPO}
                onChange={(e) => setIsUsingMPO(!isUsingMPO)}
                marginBottom={3}
              />
              {
                !isUsingMPO ? (
                  <>
                  <Input 
                    value={customOracleAddress}
                    onChange={(e) => setCustomOracleAddress(e.target.value)}
                  />
                  <Text mt={3} opacity="0.6" fontSize="sm">
                    Please make sure you know what you're doing.
                  </Text>
                  </>
                ) 
                : null
              }
              
            </Box>
          </OptionRow>
        </Column>
      </DashboardBox>

      <DashboardBox
        width="100%"
        height="60px"
        mt={4}
        py={3}
        fontSize="xl"
        as="button"
        onClick={useAuthedCallback(onDeploy)}
      >
        <Center expand fontWeight="bold">
          {isCreating ? <Spinner /> : t("Create")}
        </Center>
      </DashboardBox>
    </>
  );
}
Example #27
Source File: index.tsx    From jsonschema-editor-react with Apache License 2.0 4 votes vote down vote up
SchemaItem: React.FunctionComponent<SchemaItemProps> = (
	props: React.PropsWithChildren<SchemaItemProps>
) => {
	const {
		name,
		itemStateProp,
		showadvanced,
		required,
		parentStateProp,
		isReadOnly,
	} = props;

	// const itemState = useState(itemStateProp);
	const parentState = useState(parentStateProp);
	const parentStateOrNull: State<JSONSchema7> | undefined = parentState.ornull;
	const propertiesOrNull:
		| State<{
				[key: string]: JSONSchema7Definition;
		  }>
		| undefined = parentStateOrNull.properties.ornull;

	const nameState = useState(name);
	const isReadOnlyState = useState(isReadOnly);

	const itemState = useState(
		(parentStateProp.properties as State<{
			[key: string]: JSONSchema7;
		}>).nested(nameState.value)
	);

	const { length } = parentState.path.filter((name) => name !== "properties");
	const tagPaddingLeftStyle = {
		paddingLeft: `${20 * (length + 1)}px`,
	};

	const isRequired = required
		? required.length > 0 && required.includes(name)
		: false;
	const toast = useToast();

	// Debounce callback
	const debounced = useDebouncedCallback(
		// function
		(newValue: string) => {
			// Todo: make toast for duplicate properties
			if (propertiesOrNull && propertiesOrNull[newValue].value) {
				toast({
					title: "Duplicate Property",
					description: "Property already exists!",
					status: "error",
					duration: 1000,
					isClosable: true,
					position: "top",
				});
			} else {
				const oldName = name;
				const proptoupdate = newValue;

				const newobj = renameKeys(
					{ [oldName]: proptoupdate },
					parentState.properties.value
				);
				parentStateOrNull.properties.set(JSON.parse(JSON.stringify(newobj)));
			}
		},
		// delay in ms
		1000
	);

	if (!itemState.value) {
		return <></>;
	}

	return (
		<div>
			<Flex
				alignContent="space-evenly"
				direction="row"
				wrap="nowrap"
				className="schema-item"
				style={tagPaddingLeftStyle}
			>
				<Input
					isDisabled={isReadOnlyState.value}
					defaultValue={nameState.value}
					size="sm"
					margin={2}
					variant="outline"
					placeholder="Enter property name"
					onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
						debounced(evt.target.value);
					}}
				/>
				<Checkbox
					isDisabled={isReadOnlyState.value}
					isChecked={isRequired}
					margin={2}
					colorScheme="blue"
					onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
						if (!evt.target.checked && required.includes(name)) {
							(parentState.required as State<string[]>)[
								required.indexOf(name)
							].set(none);
						} else {
							parentState.required.merge([name]);
						}
					}}
				/>
				<Select
					isDisabled={false}
					variant="outline"
					value={itemState.type.value}
					size="sm"
					margin={2}
					placeholder="Choose data type"
					onChange={(evt: React.ChangeEvent<HTMLSelectElement>) => {
						const newSchema = handleTypeChange(
							evt.target.value as JSONSchema7TypeName,
							false
						);
						itemState.set(newSchema as JSONSchema7);
					}}
				>
					{SchemaTypes.map((item, index) => {
						return (
							<option key={String(index)} value={item}>
								{item}
							</option>
						);
					})}
				</Select>
				<Input
					isDisabled={isReadOnlyState.value}
					value={itemState.title.value || ""}
					size="sm"
					margin={2}
					variant="outline"
					placeholder="Add Title"
					onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
						itemState.title.set(evt.target.value);
					}}
				/>
				<Input
					isDisabled={isReadOnlyState.value}
					value={itemState.description.value || ""}
					size="sm"
					margin={2}
					variant="outline"
					placeholder="Add Description"
					onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
						itemState.description.set(evt.target.value);
					}}
				/>

				{itemState.type.value !== "object" && itemState.type.value !== "array" && (
					<Tooltip
						hasArrow
						aria-label="Advanced Settings"
						label="Advanced Settings"
						placement="top"
					>
						<IconButton
							isRound
							isDisabled={isReadOnlyState.value}
							size="sm"
							mt={2}
							mb={2}
							ml={1}
							variant="link"
							colorScheme="blue"
							fontSize="16px"
							icon={<FiSettings />}
							aria-label="Advanced Settings"
							onClick={() => {
								showadvanced(name);
							}}
						/>
					</Tooltip>
				)}

				<Tooltip
					hasArrow
					aria-label="Remove Node"
					label="Remove Node"
					placement="top"
				>
					<IconButton
						isRound
						isDisabled={isReadOnlyState.value}
						size="sm"
						mt={2}
						mb={2}
						ml={1}
						variant="link"
						colorScheme="red"
						fontSize="16px"
						icon={<AiOutlineDelete />}
						aria-label="Remove Node"
						onClick={() => {
							const updatedState = deleteKey(
								nameState.value,
								JSON.parse(JSON.stringify(parentState.properties.value))
							);
							parentState.properties.set(updatedState);
						}}
					/>
				</Tooltip>

				{itemState.type?.value === "object" ? (
					<DropPlus
						isDisabled={isReadOnlyState.value}
						parentStateProp={parentState}
						itemStateProp={itemStateProp}
					/>
				) : (
					<Tooltip
						hasArrow
						aria-label="Add Sibling Node"
						label="Add Sibling Node"
						placement="top"
					>
						<IconButton
							isRound
							isDisabled={isReadOnlyState.value}
							size="sm"
							mt={2}
							mb={2}
							mr={2}
							variant="link"
							colorScheme="green"
							fontSize="16px"
							icon={<IoIosAddCircleOutline />}
							aria-label="Add Sibling Node"
							onClick={() => {
								if (propertiesOrNull) {
									const fieldName = `field_${random()}`;
									propertiesOrNull
										?.nested(fieldName)
										.set(getDefaultSchema(DataType.string) as JSONSchema7);
								}
							}}
						/>
					</Tooltip>
				)}
			</Flex>
			{itemState.type?.value === "object" && (
				<SchemaObject isReadOnly={isReadOnlyState} schemaState={itemState} />
			)}
			{itemState.type?.value === "array" && (
				<SchemaArray isReadOnly={isReadOnlyState} schemaState={itemState} />
			)}
		</div>
	);
}
Example #28
Source File: notes-list.tsx    From notebook with MIT License 4 votes vote down vote up
NotesList: React.SFC<NotesListProps> = ({
  notes,
  handleClick,
  setNotes
}) => {
  const bg = useColorModeValue("white", "#2f3244");
  const [selectedNote, setSelectedNote] = React.useState<note>();
  const toast = useToast();
  const { isOpen, onOpen, onClose } = useDisclosure();

  const onDelete = (
    id: string,
    e: React.MouseEvent<SVGElement, MouseEvent>
  ) => {
    const newNotes: note[] = notes.filter((note: note) => note.id !== id);
    setNotes(newNotes);
    showToast();
    e.stopPropagation();
  };

  const onClick = (id: string, e: React.MouseEvent<SVGElement, MouseEvent>) => {
    handleClick(id);
    e.stopPropagation();
  };

  const handleSelectedNote = (note: note) => {
    setSelectedNote(note);
    onOpen();
  };

  const showToast = () => {
    toast({
      title: "Note deleted.",
      status: "success",
      position: "top",
      duration: 2000,
      isClosable: true
    });
  };

  return (
    <>
      <AnimateSharedLayout type="crossfade">
        <Box minH={"50vh"}>
          {/* <SimpleGrid
            columns={[1, 2, 2, 3]}
            mt="40px"
            gridGap="10px"
            position="relative"
            overflow="hidden"
          > */}
          <StackGrid columnWidth={330}>
            {notes.map(note => (
              <Fade in={true}>
                <motion.div
                  whileHover={{ y: -10 }}
                  layoutId={note.id}
                  onClick={() => handleSelectedNote(note)}
                >
                  <Center py={2} px={2} key={note.id}>
                    <Box
                      maxH={"400px"}
                      w="100%"
                      boxShadow={"lg"}
                      rounded={"md"}
                      p={6}
                      overflow={"hidden"}
                      cursor="pointer"
                      _hover={{ boxShadow: "xl" }}
                      bg={bg}
                      role="group"
                      // onClick={() => handleClick(note.id, true)}
                    >
                      <Stack>
                        <Flex
                          _groupHover={{ justifyContent: "space-between" }}
                          justifyContent="center"
                          align="center"
                        >
                          <Box>
                            <Text
                              color={"green.500"}
                              textTransform={"uppercase"}
                              fontWeight={800}
                              fontSize={"sm"}
                              letterSpacing={1.1}
                            >
                              Note
                            </Text>
                          </Box>
                          <Box
                            _groupHover={{ display: "block" }}
                            display="none"
                          >
                            <HStack spacing="2">
                              <Icon
                                color={"green.500"}
                                _hover={{ color: "green.600" }}
                                _groupHover={{ display: "block" }}
                                as={EditIcon}
                                w={4}
                                h={4}
                                onClick={e => onClick(note.id, e)}
                              />
                              <Icon
                                color={"green.500"}
                                _hover={{ color: "#ca364a" }}
                                _groupHover={{ display: "block" }}
                                as={DeleteIcon}
                                w={4}
                                h={4}
                                onClick={e => onDelete(note.id, e)}
                              />
                            </HStack>
                          </Box>
                        </Flex>
                        <Heading
                          fontSize={"xl"}
                          fontFamily={"body"}
                          textTransform="capitalize"
                          noOfLines={2}
                        >
                          {note.title}
                        </Heading>

                        <Text
                          color={"gray.500"}
                          fontSize="md"
                          noOfLines={{ base: 3, md: 4 }}
                        >
                          {note.body}
                        </Text>
                      </Stack>
                    </Box>
                  </Center>
                </motion.div>
              </Fade>
            ))}
          </StackGrid>
          {/* </SimpleGrid> */}
        </Box>
        {isOpen ? (
          <NoteModal
            isOpen={isOpen}
            onClose={onClose}
            selectedNote={selectedNote}
          />
        ) : (
          ""
        )}
      </AnimateSharedLayout>
    </>
  );
}