@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 |
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 |
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 |
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 : <>, </>}
</Text>
))}
</Text>
) : null}
</>
);
}
Example #4
Source File: [id].tsx From next-crud with MIT License | 6 votes |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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>
</>
);
}